From fe40f488dddc35c72f77fff508e2a1b6996eb22f Mon Sep 17 00:00:00 2001 From: Eric Amodio Date: Thu, 2 Nov 2017 03:23:27 -0400 Subject: [PATCH] Updates configuration usage for multi-root Uses new ConfigurationChangeEvent arg --- package.json | 261 +++++++++++++------- src/annotations/annotationController.ts | 105 ++++---- src/annotations/annotationProvider.ts | 8 +- src/annotations/blameAnnotationProvider.ts | 2 +- src/annotations/recentChangesAnnotationProvider.ts | 2 +- src/codeLensController.ts | 71 +++--- src/configuration.ts | 271 ++++++++++++++++++--- src/currentLineController.ts | 56 +++-- src/extension.ts | 6 +- src/git/git.ts | 21 +- src/git/gitContextTracker.ts | 67 ++--- src/git/models/remote.ts | 9 +- src/git/models/repository.ts | 64 ++++- src/git/parsers/remoteParser.ts | 5 +- src/git/remotes/custom.ts | 25 +- src/git/remotes/factory.ts | 50 ++-- src/gitCodeLensProvider.ts | 111 +++++---- src/gitRevisionCodeLensProvider.ts | 20 +- src/gitService.ts | 121 ++++----- src/logger.ts | 73 +++--- src/system/function.ts | 17 ++ src/views/branchHistoryNode.ts | 20 +- src/views/branchesNode.ts | 19 +- src/views/commitFileNode.ts | 20 +- src/views/commitNode.ts | 24 +- src/views/fileHistoryNode.ts | 2 +- src/views/folderNode.ts | 7 +- src/views/gitExplorer.ts | 157 ++++++------ src/views/remoteNode.ts | 13 +- src/views/remotesNode.ts | 17 +- src/views/repositoryNode.ts | 6 +- src/views/stashFileNode.ts | 15 +- src/views/stashNode.ts | 16 +- src/views/stashesNode.ts | 17 +- src/views/statusFileCommitsNode.ts | 16 +- src/views/statusFilesNode.ts | 26 +- src/views/statusNode.ts | 6 +- src/views/statusUpstreamNode.ts | 20 +- 38 files changed, 1086 insertions(+), 680 deletions(-) diff --git a/package.json b/package.json index eb3f0b6..4df23b0 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ }, "publisher": "eamodio", "engines": { - "vscode": "^1.17.2" + "vscode": "^1.18.0" }, "license": "SEE LICENSE IN LICENSE", "displayName": "Git Lens \u2014 git blame annotations, code lens, and more", @@ -23,14 +23,15 @@ "Other" ], "keywords": [ - "git", "gitlens", + "git", "blame", "log", "annotation", "diff", "lens", - "history" + "history", + "multi-root ready" ], "galleryBanner": { "color": "#56098c", @@ -55,12 +56,14 @@ "gitlens.debug": { "type": "boolean", "default": false, - "description": "Specifies debug mode" + "description": "Specifies debug mode", + "scope": "window" }, "gitlens.insiders": { "type": "boolean", "default": false, - "description": "Specifies whether or not to enable new experimental features (expect there to be issues)" + "description": "Specifies whether or not to enable new experimental features (expect there to be issues)", + "scope": "window" }, "gitlens.outputLevel": { "type": "string", @@ -70,27 +73,32 @@ "errors", "verbose" ], - "description": "Specifies how much (if any) output will be sent to the GitLens output channel" + "description": "Specifies how much (if any) output will be sent to the GitLens output channel", + "scope": "window" }, "gitlens.annotations.file.gutter.format": { "type": "string", "default": "${message|40?} ${ago|14-}", - "description": "Specifies the format of the gutter blame annotations\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.annotations.file.gutter.dateFormat`)\n ${authorAgo} - commit author, relative commit date\nSee https://github.com/eamodio/vscode-gitlens/wiki/Advanced-Formatting for advanced formatting" + "description": "Specifies the format of the gutter blame annotations\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.annotations.file.gutter.dateFormat`)\n ${authorAgo} - commit author, relative commit date\nSee https://github.com/eamodio/vscode-gitlens/wiki/Advanced-Formatting for advanced formatting", + "scope": "window" }, "gitlens.annotations.file.gutter.dateFormat": { "type": "string", "default": null, - "description": "Specifies how to format absolute dates (using the `${date}` token) in gutter blame annotations\nSee https://momentjs.com/docs/#/displaying/format/ for valid formats" + "description": "Specifies how to format absolute dates (using the `${date}` token) in gutter blame annotations\nSee https://momentjs.com/docs/#/displaying/format/ for valid formats", + "scope": "window" }, "gitlens.annotations.file.gutter.compact": { "type": "boolean", "default": true, - "description": "Specifies whether or not to compact (deduplicate) matching adjacent gutter blame annotations" + "description": "Specifies whether or not to compact (deduplicate) matching adjacent gutter blame annotations", + "scope": "window" }, "gitlens.annotations.file.gutter.heatmap.enabled": { "type": "boolean", "default": true, - "description": "Specifies whether or not to provide a heatmap indicator in the gutter blame annotations" + "description": "Specifies whether or not to provide a heatmap indicator in the gutter blame annotations", + "scope": "window" }, "gitlens.annotations.file.gutter.heatmap.location": { "type": "string", @@ -99,87 +107,104 @@ "left", "right" ], - "description": "Specifies where the heatmap indicators will be shown in the gutter blame annotations\n `left` - adds a heatmap indicator on the left edge of the gutter blame annotations\n `right` - adds a heatmap indicator on the right edge of the gutter blame annotations" + "description": "Specifies where the heatmap indicators will be shown in the gutter blame annotations\n `left` - adds a heatmap indicator on the left edge of the gutter blame annotations\n `right` - adds a heatmap indicator on the right edge of the gutter blame annotations", + "scope": "window" }, "gitlens.annotations.file.gutter.hover.details": { "type": "boolean", "default": true, - "description": "Specifies whether or not to provide a commit details hover annotation over the gutter blame annotations" + "description": "Specifies whether or not to provide a commit details hover annotation over the gutter blame annotations", + "scope": "window" }, "gitlens.annotations.file.gutter.hover.changes": { "type": "boolean", "default": true, - "description": "Specifies whether or not to provide a changes (diff) hover annotation over the gutter blame annotations" + "description": "Specifies whether or not to provide a changes (diff) hover annotation over the gutter blame annotations", + "scope": "window" }, "gitlens.annotations.file.gutter.hover.wholeLine": { "type": "boolean", "default": true, - "description": "Specifies whether or not to trigger hover annotations over the whole line" + "description": "Specifies whether or not to trigger hover annotations over the whole line", + "scope": "window" }, "gitlens.annotations.file.hover.details": { "type": "boolean", "default": true, - "description": "Specifies whether or not to provide a commit details hover annotation over each line" + "description": "Specifies whether or not to provide a commit details hover annotation over each line", + "scope": "window" }, "gitlens.annotations.file.hover.changes": { "type": "boolean", "default": true, - "description": "Specifies whether or not to provide a changes (diff) hover annotation over each line" + "description": "Specifies whether or not to provide a changes (diff) hover annotation over each line", + "scope": "window" }, "gitlens.annotations.file.hover.heatmap.enabled": { "type": "boolean", "default": true, - "description": "Specifies whether or not to provide heatmap indicators on the left edge of each line" + "description": "Specifies whether or not to provide heatmap indicators on the left edge of each line", + "scope": "window" }, "gitlens.annotations.file.recentChanges.hover.details": { "type": "boolean", "default": true, - "description": "Specifies whether or not to provide a commit details hover annotation" + "description": "Specifies whether or not to provide a commit details hover annotation", + "scope": "window" }, "gitlens.annotations.file.recentChanges.hover.changes": { "type": "boolean", "default": true, - "description": "Specifies whether or not to provide a changes (diff) hover annotation" + "description": "Specifies whether or not to provide a changes (diff) hover annotation", + "scope": "window" }, "gitlens.annotations.line.hover.details": { "type": "boolean", "default": true, - "description": "Specifies whether or not to provide a commit details hover annotation for the current line" + "description": "Specifies whether or not to provide a commit details hover annotation for the current line", + "scope": "window" }, "gitlens.annotations.line.hover.changes": { "type": "boolean", "default": true, - "description": "Specifies whether or not to provide a changes (diff) hover annotation for the current line" + "description": "Specifies whether or not to provide a changes (diff) hover annotation for the current line", + "scope": "window" }, "gitlens.annotations.line.trailing.format": { "type": "string", "default": "${authorAgo} \u2022 ${message}", - "description": "Specifies the format of the trailing blame annotations\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.annotations.line.trailing.dateFormat`)\n ${authorAgo} - commit author, relative commit date\nSee https://github.com/eamodio/vscode-gitlens/wiki/Advanced-Formatting for advanced formatting" + "description": "Specifies the format of the trailing blame annotations\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.annotations.line.trailing.dateFormat`)\n ${authorAgo} - commit author, relative commit date\nSee https://github.com/eamodio/vscode-gitlens/wiki/Advanced-Formatting for advanced formatting", + "scope": "window" }, "gitlens.annotations.line.trailing.dateFormat": { "type": "string", "default": null, - "description": "Specifies how to format absolute dates (using the `${date}` token) in trailing blame annotations\nSee https://momentjs.com/docs/#/displaying/format/ for valid formats" + "description": "Specifies how to format absolute dates (using the `${date}` token) in trailing blame annotations\nSee https://momentjs.com/docs/#/displaying/format/ for valid formats", + "scope": "window" }, "gitlens.annotations.line.trailing.hover.details": { "type": "boolean", "default": true, - "description": "Specifies whether or not to provide a commit details hover annotation over the trailing blame annotations" + "description": "Specifies whether or not to provide a commit details hover annotation over the trailing blame annotations", + "scope": "window" }, "gitlens.annotations.line.trailing.hover.changes": { "type": "boolean", "default": true, - "description": "Specifies whether or not to provide a changes (diff) hover annotation over the trailing blame annotations" + "description": "Specifies whether or not to provide a changes (diff) hover annotation over the trailing blame annotations", + "scope": "window" }, "gitlens.annotations.line.trailing.hover.wholeLine": { "type": "boolean", "default": false, - "description": "Specifies whether or not to trigger hover annotations over the whole line" + "description": "Specifies whether or not to trigger hover annotations over the whole line", + "scope": "window" }, "gitlens.blame.ignoreWhitespace": { "type": "boolean", "default": false, - "description": "Specifies whether or not to ignore whitespace when comparing revisions during blame operations" + "description": "Specifies whether or not to ignore whitespace when comparing revisions during blame operations", + "scope": "resource" }, "gitlens.blame.file.annotationType": { "type": "string", @@ -188,12 +213,14 @@ "gutter", "hover" ], - "description": "Specifies the type of blame annotations that will be shown for the current file\n `gutter` - adds an annotation to the beginning of each line\n `hover` - shows annotations when hovering over each line" + "description": "Specifies the type of blame annotations that will be shown for the current file\n `gutter` - adds an annotation to the beginning of each line\n `hover` - shows annotations when hovering over each line", + "scope": "window" }, "gitlens.blame.file.lineHighlight.enabled": { "type": "boolean", "default": true, - "description": "Specifies whether or not to highlight lines associated with the current line" + "description": "Specifies whether or not to highlight lines associated with the current line", + "scope": "window" }, "gitlens.blame.file.lineHighlight.locations": { "type": "array", @@ -213,12 +240,14 @@ "minItems": 1, "maxItems": 3, "uniqueItems": true, - "description": "Specifies where the associated line highlights will be shown\n `gutter` - adds a gutter glyph\n `line` - adds a full-line highlight background color\n `overviewRuler` - adds a decoration to the overviewRuler (scroll bar)" + "description": "Specifies where the associated line highlights will be shown\n `gutter` - adds a gutter glyph\n `line` - adds a full-line highlight background color\n `overviewRuler` - adds a decoration to the overviewRuler (scroll bar)", + "scope": "window" }, "gitlens.blame.line.enabled": { "type": "boolean", "default": true, - "description": "Specifies whether or not to provide a blame annotation for the current line, by default\nUse the `gitlens.toggleLineBlame` command to toggle the annotations on and off for the current session" + "description": "Specifies whether or not to provide a blame annotation for the current line, by default\nUse the `gitlens.toggleLineBlame` command to toggle the annotations on and off for the current session", + "scope": "window" }, "gitlens.blame.line.annotationType": { "type": "string", @@ -227,7 +256,8 @@ "trailing", "hover" ], - "description": "Specifies the type of blame annotations that will be shown for the current line\n `trailing` - adds an annotation to the end of the current line\n `hover` - shows annotations when hovering over the current line" + "description": "Specifies the type of blame annotations that will be shown for the current line\n `trailing` - adds an annotation to the end of the current line\n `hover` - shows annotations when hovering over the current line", + "scope": "window" }, "gitlens.recentChanges.file.lineHighlight.locations": { "type": "array", @@ -247,17 +277,20 @@ "minItems": 1, "maxItems": 3, "uniqueItems": true, - "description": "Specifies where the highlights of the recently changed lines will be shown\n `gutter` - adds a gutter glyph\n `line` - adds a full-line highlight background color\n `overviewRuler` - adds a decoration to the overviewRuler (scroll bar)" + "description": "Specifies where the highlights of the recently changed lines will be shown\n `gutter` - adds a gutter glyph\n `line` - adds a full-line highlight background color\n `overviewRuler` - adds a decoration to the overviewRuler (scroll bar)", + "scope": "window" }, "gitlens.codeLens.enabled": { "type": "boolean", "default": true, - "description": "Specifies whether or not to provide any Git code lens, by default\nUse the `gitlens.toggleCodeLens` command to toggle the Git code lens on and off for the current session" + "description": "Specifies whether or not to provide any Git code lens, by default\nUse the `gitlens.toggleCodeLens` command to toggle the Git code lens on and off for the current session", + "scope": "window" }, "gitlens.codeLens.recentChange.enabled": { "type": "boolean", "default": true, - "description": "Specifies whether or not to show a `recent change` code lens showing the author and date of the most recent commit for the file or code block" + "description": "Specifies whether or not to show a `recent change` code lens showing the author and date of the most recent commit for the file or code block", + "scope": "window" }, "gitlens.codeLens.recentChange.command": { "type": "string", @@ -270,12 +303,14 @@ "gitlens.showQuickFileHistory", "gitlens.showQuickRepoHistory" ], - "description": "Specifies the command to be executed when the `recent change` code lens is clicked\n `gitlens.toggleFileBlame` - toggles file blame annotations\n `gitlens.diffWithPrevious` - compares the current committed file with the previous commit\n `gitlens.showQuickCommitDetails` - shows a commit details quick pick\n `gitlens.showQuickCommitFileDetails` - shows a commit file details quick pick\n `gitlens.showQuickFileHistory` - shows a file history quick pick\n `gitlens.showQuickRepoHistory` - shows a branch history quick pick" + "description": "Specifies the command to be executed when the `recent change` code lens is clicked\n `gitlens.toggleFileBlame` - toggles file blame annotations\n `gitlens.diffWithPrevious` - compares the current committed file with the previous commit\n `gitlens.showQuickCommitDetails` - shows a commit details quick pick\n `gitlens.showQuickCommitFileDetails` - shows a commit file details quick pick\n `gitlens.showQuickFileHistory` - shows a file history quick pick\n `gitlens.showQuickRepoHistory` - shows a branch history quick pick", + "scope": "window" }, "gitlens.codeLens.authors.enabled": { "type": "boolean", "default": true, - "description": "Specifies whether or not to show an `authors` code lens showing number of authors of the file or code block and the most prominent author (if there is more than one)" + "description": "Specifies whether or not to show an `authors` code lens showing number of authors of the file or code block and the most prominent author (if there is more than one)", + "scope": "window" }, "gitlens.codeLens.authors.command": { "type": "string", @@ -288,7 +323,8 @@ "gitlens.showQuickFileHistory", "gitlens.showQuickRepoHistory" ], - "description": "Specifies the command to be executed when the `authors` code lens is clicked\n `gitlens.toggleFileBlame` - toggles file blame annotations\n `gitlens.diffWithPrevious` - compares the current committed file with the previous commit\n `gitlens.showQuickCommitDetails` - shows a commit details quick pick\n `gitlens.showQuickCommitFileDetails` - shows a commit file details quick pick\n `gitlens.showQuickFileHistory` - shows a file history quick pick\n `gitlens.showQuickRepoHistory` - shows a branch history quick pick" + "description": "Specifies the command to be executed when the `authors` code lens is clicked\n `gitlens.toggleFileBlame` - toggles file blame annotations\n `gitlens.diffWithPrevious` - compares the current committed file with the previous commit\n `gitlens.showQuickCommitDetails` - shows a commit details quick pick\n `gitlens.showQuickCommitFileDetails` - shows a commit file details quick pick\n `gitlens.showQuickFileHistory` - shows a file history quick pick\n `gitlens.showQuickRepoHistory` - shows a branch history quick pick", + "scope": "window" }, "gitlens.codeLens.locations": { "type": "array", @@ -307,7 +343,8 @@ "minItems": 1, "maxItems": 4, "uniqueItems": true, - "description": "Specifies where Git code lens will be shown in the document\n `document` - adds code lens at the top of the document\n `containers` - adds code lens at the start of container-like symbols (modules, classes, interfaces, etc)\n `blocks` - adds code lens at the start of block-like symbols (functions, methods, etc) lines" + "description": "Specifies where Git code lens will be shown in the document\n `document` - adds code lens at the top of the document\n `containers` - adds code lens at the start of container-like symbols (modules, classes, interfaces, etc)\n `blocks` - adds code lens at the start of block-like symbols (functions, methods, etc) lines", + "scope": "resource" }, "gitlens.codeLens.customLocationSymbols": { "type": "array", @@ -315,7 +352,8 @@ "type": "string" }, "uniqueItems": true, - "description": "Specifies a set of document symbols where Git code lens will or will not be shown in the document\nPrefix with `!` to not show Git code lens for the symbol\nMust be a member of `SymbolKind`" + "description": "Specifies a set of document symbols where Git code lens will or will not be shown in the document\nPrefix with `!` to not show Git code lens for the symbol\nMust be a member of `SymbolKind`", + "scope": "resource" }, "gitlens.codeLens.perLanguageLocations": { "type": "array", @@ -405,37 +443,44 @@ } }, "uniqueItems": true, - "description": "Specifies where Git code lens will be shown in the document for the specified languages" + "description": "Specifies where Git code lens will be shown in the document for the specified languages", + "scope": "resource" }, "gitlens.codeLens.debug": { "type": "boolean", "default": false, - "description": "Specifies whether or not to show debug information in code lens" + "description": "Specifies whether or not to show debug information in code lens", + "scope": "window" }, "gitlens.defaultDateFormat": { "type": "string", "default": null, - "description": "Specifies how all absolute dates will be formatted by default\nSee https://momentjs.com/docs/#/displaying/format/ for valid formats" + "description": "Specifies how all absolute dates will be formatted by default\nSee https://momentjs.com/docs/#/displaying/format/ for valid formats", + "scope": "window" }, "gitlens.gitExplorer.autoRefresh": { "type": "boolean", "default": true, - "description": "Specifies whether or not to automatically refresh the `GitLens` custom view when the repository or the file system changes" + "description": "Specifies whether or not to automatically refresh the `GitLens` custom view when the repository or the file system changes", + "scope": "window" }, "gitlens.gitExplorer.commitFormat": { "type": "string", "default": "${message} \u00a0\u2022\u00a0 ${authorAgo} \u00a0 (${id})", - "description": "Specifies the format of committed changes in the `GitLens` custom view\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.statusBar.dateFormat`)\n ${authorAgo} - commit author, relative commit date\nSee https://github.com/eamodio/vscode-gitlens/wiki/Advanced-Formatting for advanced formatting" + "description": "Specifies the format of committed changes in the `GitLens` custom view\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.statusBar.dateFormat`)\n ${authorAgo} - commit author, relative commit date\nSee https://github.com/eamodio/vscode-gitlens/wiki/Advanced-Formatting for advanced formatting", + "scope": "window" }, "gitlens.gitExplorer.commitFileFormat": { "type": "string", "default": "${filePath}", - "description": "Specifies the format of a committed file in the `GitLens` custom view\nAvailable tokens\n ${directory} - directory name\n ${file} - file name\n ${filePath} - formatted file name and path\n ${path} - full file path" + "description": "Specifies the format of a committed file in the `GitLens` custom view\nAvailable tokens\n ${directory} - directory name\n ${file} - file name\n ${filePath} - formatted file name and path\n ${path} - full file path", + "scope": "window" }, "gitlens.gitExplorer.enabled": { "type": "boolean", "default": true, - "description": "Specifies whether or not to show the `GitLens` custom view" + "description": "Specifies whether or not to show the `GitLens` custom view", + "scope": "window" }, "gitlens.gitExplorer.files.layout": { "type": "string", @@ -445,42 +490,50 @@ "list", "tree" ], - "description": "Specifies how the `GitLens` custom view will display files\n `auto` - automatically switches between displaying files as a `tree` or `list` based on the `gitlens.gitExplorer.files.threshold` setting and the number of files at each nesting level\n `list` - displays files as a list\n `tree` - displays files as a tree" + "description": "Specifies how the `GitLens` custom view will display files\n `auto` - automatically switches between displaying files as a `tree` or `list` based on the `gitlens.gitExplorer.files.threshold` setting and the number of files at each nesting level\n `list` - displays files as a list\n `tree` - displays files as a tree", + "scope": "window" }, "gitlens.gitExplorer.files.compact": { "type": "boolean", "default": true, - "description": "Specifies whether or not to compact (flatten) unnecessary file nesting in the `GitLens` custom view\nOnly applies when displaying files as a `tree` or `auto`" + "description": "Specifies whether or not to compact (flatten) unnecessary file nesting in the `GitLens` custom view\nOnly applies when displaying files as a `tree` or `auto`", + "scope": "window" }, "gitlens.gitExplorer.files.threshold": { "type": "number", "default": 5, - "description": "Specifies when to switch between displaying files as a `tree` or `list` based on the number of files in a nesting level in the `GitLens` custom view\nOnly applies when displaying files as `auto`" + "description": "Specifies when to switch between displaying files as a `tree` or `list` based on the number of files in a nesting level in the `GitLens` custom view\nOnly applies when displaying files as `auto`", + "scope": "window" }, "gitlens.gitExplorer.includeWorkingTree": { "type": "boolean", "default": true, - "description": "Specifies whether or not to include working tree files inside the `Repository Status` node of the `GitLens` custom view" + "description": "Specifies whether or not to include working tree files inside the `Repository Status` node of the `GitLens` custom view", + "scope": "window" }, "gitlens.gitExplorer.showTrackingBranch": { "type": "boolean", "default": true, - "description": "Specifies whether or not to show the tracking branch when displaying local branches in the `GitLens` custom view" + "description": "Specifies whether or not to show the tracking branch when displaying local branches in the `GitLens` custom view", + "scope": "window" }, "gitlens.gitExplorer.stashFormat": { "type": "string", "default": "${message}", - "description": "Specifies the format of stashed changes in the `GitLens` custom view\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.statusBar.dateFormat`)\n ${authorAgo} - commit author, relative commit date\nSee https://github.com/eamodio/vscode-gitlens/wiki/Advanced-Formatting for advanced formatting" + "description": "Specifies the format of stashed changes in the `GitLens` custom view\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.statusBar.dateFormat`)\n ${authorAgo} - commit author, relative commit date\nSee https://github.com/eamodio/vscode-gitlens/wiki/Advanced-Formatting for advanced formatting", + "scope": "window" }, "gitlens.gitExplorer.stashFileFormat": { "type": "string", "default": "${filePath}", - "description": "Specifies the format of a stashed file in the `GitLens` custom view\nAvailable tokens\n ${directory} - directory name\n ${file} - file name\n ${filePath} - formatted file name and path\n ${path} - full file path" + "description": "Specifies the format of a stashed file in the `GitLens` custom view\nAvailable tokens\n ${directory} - directory name\n ${file} - file name\n ${filePath} - formatted file name and path\n ${path} - full file path", + "scope": "window" }, "gitlens.gitExplorer.statusFileFormat": { "type": "string", "default": "${working}${filePath}", - "description": "Specifies the format of the status of a working or committed file in the `GitLens` custom view\nAvailable tokens\n ${directory} - directory name\n ${file} - file name\n ${filePath} - formatted file name and path\n ${path} - full file path\n ${working} - optional indicator if the file is uncommitted" + "description": "Specifies the format of the status of a working or committed file in the `GitLens` custom view\nAvailable tokens\n ${directory} - directory name\n ${file} - file name\n ${filePath} - formatted file name and path\n ${path} - full file path\n ${working} - optional indicator if the file is uncommitted", + "scope": "window" }, "gitlens.gitExplorer.view": { "type": "string", @@ -490,7 +543,8 @@ "history", "repository" ], - "description": "Specifies the starting view (mode) of the `GitLens` custom view\n `auto` - shows the last selected view, defaults to `repository`\n `history` - shows the commit history of the active file\n `repository` - shows a repository explorer" + "description": "Specifies the starting view (mode) of the `GitLens` custom view\n `auto` - shows the last selected view, defaults to `repository`\n `history` - shows the commit history of the active file\n `repository` - shows a repository explorer", + "scope": "window" }, "gitlens.remotes": { "type": "array", @@ -577,12 +631,14 @@ } }, "uniqueItems": true, - "description": "Specifies user-defined remote (code-hosting) services or custom domains for built-in remote services" + "description": "Specifies user-defined remote (code-hosting) services or custom domains for built-in remote services", + "scope": "resource" }, "gitlens.statusBar.enabled": { "type": "boolean", "default": true, - "description": "Specifies whether or not to provide blame information on the status bar" + "description": "Specifies whether or not to provide blame information on the status bar", + "scope": "window" }, "gitlens.statusBar.alignment": { "type": "string", @@ -591,7 +647,8 @@ "left", "right" ], - "description": "Specifies the blame alignment in the status bar\n `left` - align to the left\n `right` - align to the right" + "description": "Specifies the blame alignment in the status bar\n `left` - align to the left\n `right` - align to the right", + "scope": "window" }, "gitlens.statusBar.command": { "type": "string", @@ -606,127 +663,152 @@ "gitlens.showQuickFileHistory", "gitlens.showQuickRepoHistory" ], - "description": "Specifies the command to be executed when the blame status bar item is clicked\n `gitlens.toggleFileBlame` - toggles file blame annotations\n `gitlens.diffWithPrevious` - compares the current line commit with the previous\n `gitlens.diffWithWorking` - compares the current line commit with the working tree\n `gitlens.toggleCodeLens` - toggles Git code lens\n `gitlens.showQuickCommitDetails` - shows a commit details quick pick\n `gitlens.showQuickCommitFileDetails` - shows a commit file details quick pick\n `gitlens.showQuickFileHistory` - shows a file history quick pick\n `gitlens.showQuickRepoHistory` - shows a branch history quick pick" + "description": "Specifies the command to be executed when the blame status bar item is clicked\n `gitlens.toggleFileBlame` - toggles file blame annotations\n `gitlens.diffWithPrevious` - compares the current line commit with the previous\n `gitlens.diffWithWorking` - compares the current line commit with the working tree\n `gitlens.toggleCodeLens` - toggles Git code lens\n `gitlens.showQuickCommitDetails` - shows a commit details quick pick\n `gitlens.showQuickCommitFileDetails` - shows a commit file details quick pick\n `gitlens.showQuickFileHistory` - shows a file history quick pick\n `gitlens.showQuickRepoHistory` - shows a branch history quick pick", + "scope": "window" }, "gitlens.statusBar.format": { "type": "string", "default": "${authorAgo}", - "description": "Specifies the format of the status bar blame information\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.statusBar.dateFormat`)\n ${authorAgo} - commit author, relative commit date\nSee https://github.com/eamodio/vscode-gitlens/wiki/Advanced-Formatting for advanced formatting" + "description": "Specifies the format of the status bar blame information\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.statusBar.dateFormat`)\n ${authorAgo} - commit author, relative commit date\nSee https://github.com/eamodio/vscode-gitlens/wiki/Advanced-Formatting for advanced formatting", + "scope": "window" }, "gitlens.statusBar.dateFormat": { "type": "string", "default": null, - "description": "Specifies the date format of absolute dates shown in the blame information on the status bar. See https://momentjs.com/docs/#/displaying/format/ for valid formats" + "description": "Specifies the date format of absolute dates shown in the blame information on the status bar. See https://momentjs.com/docs/#/displaying/format/ for valid formats", + "scope": "window" }, "gitlens.strings.codeLens.unsavedChanges.recentChangeAndAuthors": { "type": "string", "default": "Cannot determine recent change or authors (unsaved changes)", - "description": "Specifies the string to be shown in place of both the `recent change` and `authors` code lens when there are unsaved changes" + "description": "Specifies the string to be shown in place of both the `recent change` and `authors` code lens when there are unsaved changes", + "scope": "window" }, "gitlens.strings.codeLens.unsavedChanges.recentChangeOnly": { "type": "string", "default": "Cannot determine recent change (unsaved changes)", - "description": "Specifies the string to be shown in place of the `recent change` code lens when there are unsaved changes" + "description": "Specifies the string to be shown in place of the `recent change` code lens when there are unsaved changes", + "scope": "window" }, "gitlens.strings.codeLens.unsavedChanges.authorsOnly": { "type": "string", "default": "Cannot determine authors (unsaved changes)", - "description": "Specifies the string to be shown in place of the `authors` code lens when there are unsaved changes" + "description": "Specifies the string to be shown in place of the `authors` code lens when there are unsaved changes", + "scope": "window" }, "gitlens.theme.annotations.file.gutter.separateLines": { "type": "boolean", "default": true, - "description": "Specifies whether or not gutter blame annotations will be separated by a small gap" + "description": "Specifies whether or not gutter blame annotations will be separated by a small gap", + "scope": "window" }, "gitlens.theme.annotations.file.gutter.dark.backgroundColor": { "type": "string", "default": "rgba(255, 255, 255, 0.075)", - "description": "Specifies the dark theme background color of the gutter blame annotations. Must be a valid css color" + "description": "Specifies the dark theme background color of the gutter blame annotations. Must be a valid css color", + "scope": "window" }, "gitlens.theme.annotations.file.gutter.light.backgroundColor": { "type": "string", "default": "rgba(0, 0, 0, 0.05)", - "description": "Specifies the light theme background color of the gutter blame annotations. Must be a valid css color" + "description": "Specifies the light theme background color of the gutter blame annotations. Must be a valid css color", + "scope": "window" }, "gitlens.theme.annotations.file.gutter.dark.foregroundColor": { "type": "string", "default": "rgb(190, 190, 190)", - "description": "Specifies the dark theme foreground color of the gutter blame annotations. Must be a valid css color" + "description": "Specifies the dark theme foreground color of the gutter blame annotations. Must be a valid css color", + "scope": "window" }, "gitlens.theme.annotations.file.gutter.light.foregroundColor": { "type": "string", "default": "rgb(116, 116, 116)", - "description": "Specifies the light theme foreground color of the gutter blame annotations. Must be a valid css color" + "description": "Specifies the light theme foreground color of the gutter blame annotations. Must be a valid css color", + "scope": "window" }, "gitlens.theme.annotations.file.gutter.dark.uncommittedForegroundColor": { "type": "string", "default": "rgba(0, 188, 242, 0.6)", - "description": "Specifies the dark theme foreground color of an uncommitted line in the gutter blame annotations. Must be a valid css color" + "description": "Specifies the dark theme foreground color of an uncommitted line in the gutter blame annotations. Must be a valid css color", + "scope": "window" }, "gitlens.theme.annotations.file.gutter.light.uncommittedForegroundColor": { "type": "string", "default": "rgba(0, 188, 242, 0.6)", - "description": "Specifies the light theme foreground color of an uncommitted line in the gutter blame annotations. Must be a valid css color" + "description": "Specifies the light theme foreground color of an uncommitted line in the gutter blame annotations. Must be a valid css color", + "scope": "window" }, "gitlens.theme.annotations.line.trailing.dark.backgroundColor": { "type": "string", "default": null, - "description": "Specifies the dark theme background color of the trailing blame annotation. Must be a valid css color" + "description": "Specifies the dark theme background color of the trailing blame annotation. Must be a valid css color", + "scope": "window" }, "gitlens.theme.annotations.line.trailing.light.backgroundColor": { "type": "string", "default": null, - "description": "Specifies the light theme background color of the trailing blame annotation. Must be a valid css color" + "description": "Specifies the light theme background color of the trailing blame annotation. Must be a valid css color", + "scope": "window" }, "gitlens.theme.annotations.line.trailing.dark.foregroundColor": { "type": "string", "default": "rgba(153, 153, 153, 0.35)", - "description": "Specifies the dark theme foreground color of the trailing blame annotation. Must be a valid css color" + "description": "Specifies the dark theme foreground color of the trailing blame annotation. Must be a valid css color", + "scope": "window" }, "gitlens.theme.annotations.line.trailing.light.foregroundColor": { "type": "string", "default": "rgba(153, 153, 153, 0.35)", - "description": "Specifies the light theme foreground color of the trailing blame annotation. Must be a valid css color" + "description": "Specifies the light theme foreground color of the trailing blame annotation. Must be a valid css color", + "scope": "window" }, "gitlens.theme.lineHighlight.dark.backgroundColor": { "type": "string", "default": "rgba(0, 188, 242, 0.2)", - "description": "Specifies the dark theme background color of the associated line highlights in blame annotations. Must be a valid css color" + "description": "Specifies the dark theme background color of the associated line highlights in blame annotations. Must be a valid css color", + "scope": "window" }, "gitlens.theme.lineHighlight.light.backgroundColor": { "type": "string", "default": "rgba(0, 188, 242, 0.2)", - "description": "Specifies the light theme background color of the associated line highlights in blame annotations. Must be a valid css color" + "description": "Specifies the light theme background color of the associated line highlights in blame annotations. Must be a valid css color", + "scope": "window" }, "gitlens.theme.lineHighlight.dark.overviewRulerColor": { "type": "string", "default": "rgba(0, 188, 242, 0.6)", - "description": "Specifies the dark theme overview ruler color of the associated line highlights in blame annotations. Must be a valid css color" + "description": "Specifies the dark theme overview ruler color of the associated line highlights in blame annotations. Must be a valid css color", + "scope": "window" }, "gitlens.theme.lineHighlight.light.overviewRulerColor": { "type": "string", "default": "rgba(0, 188, 242, 0.6)", - "description": "Specifies the light theme overview ruler color of the associated line highlights in blame annotations. Must be a valid css color" + "description": "Specifies the light theme overview ruler color of the associated line highlights in blame annotations. Must be a valid css color", + "scope": "window" }, "gitlens.advanced.caching.enabled": { "type": "boolean", "default": true, - "description": "Specifies whether git output will be cached" + "description": "Specifies whether git output will be cached", + "scope": "window" }, "gitlens.advanced.caching.maxLines": { "type": "number", "default": 0, - "description": "Specifies the threshold for caching larger documents" + "description": "Specifies the threshold for caching larger documents", + "scope": "window" }, "gitlens.advanced.git": { "type": "string", "default": null, - "description": "Specifies the git path to use" + "description": "Specifies the git path to use", + "scope": "window" }, "gitlens.advanced.maxQuickHistory": { "type": "number", "default": 200, - "description": "Specifies the maximum number of QuickPick history entries to show" + "description": "Specifies the maximum number of QuickPick history entries to show", + "scope": "window" }, "gitlens.advanced.menus": { "type": "object", @@ -759,7 +841,6 @@ "remote": true } }, - "description": "Specifies which commands will be added to which menus", "properties": { "editorContext": { "type": "object", @@ -884,17 +965,21 @@ } } } - } + }, + "description": "Specifies which commands will be added to which menus", + "scope": "window" }, "gitlens.advanced.quickPick.closeOnFocusOut": { "type": "boolean", "default": true, - "description": "Specifies whether or not to close the QuickPick menu when focus is lost" + "description": "Specifies whether or not to close the QuickPick menu when focus is lost", + "scope": "window" }, "gitlens.advanced.telemetry.enabled": { "type": "boolean", "default": true, - "description": "Specifies whether or not to enable GitLens telemetry (even if enabled still abides by the overall `telemetry.enableTelemetry` setting" + "description": "Specifies whether or not to enable GitLens telemetry (even if enabled still abides by the overall `telemetry.enableTelemetry` setting", + "scope": "window" } } }, diff --git a/src/annotations/annotationController.ts b/src/annotations/annotationController.ts index ad9af50..fc95852 100644 --- a/src/annotations/annotationController.ts +++ b/src/annotations/annotationController.ts @@ -1,9 +1,9 @@ 'use strict'; -import { Functions, Iterables, Objects } from '../system'; -import { DecorationRangeBehavior, DecorationRenderOptions, Disposable, Event, EventEmitter, ExtensionContext, OverviewRulerLane, Progress, ProgressLocation, TextDocument, TextDocumentChangeEvent, TextEditor, TextEditorDecorationType, TextEditorViewColumnChangeEvent, window, workspace } from 'vscode'; +import { Functions, Iterables } from '../system'; +import { ConfigurationChangeEvent, DecorationRangeBehavior, DecorationRenderOptions, Disposable, Event, EventEmitter, ExtensionContext, OverviewRulerLane, Progress, ProgressLocation, TextDocument, TextDocumentChangeEvent, TextEditor, TextEditorDecorationType, TextEditorViewColumnChangeEvent, window, workspace } from 'vscode'; import { AnnotationProviderBase, TextEditorCorrelationKey } from './annotationProvider'; import { TextDocumentComparer } from '../comparers'; -import { ExtensionKey, IConfig, LineHighlightLocations, themeDefaults } from '../configuration'; +import { configuration, IConfig, LineHighlightLocations } from '../configuration'; import { CommandContext, isTextEditor, setCommandContext } from '../constants'; import { BlameabilityChangeEvent, GitContextTracker, GitService, GitUri } from '../gitService'; import { GutterBlameAnnotationProvider } from './gutterBlameAnnotationProvider'; @@ -53,7 +53,6 @@ export class AnnotationController extends Disposable { private _annotationsDisposable: Disposable | undefined; private _annotationProviders: Map = new Map(); - private _config: IConfig; private _disposable: Disposable; private _keyboardScope: KeyboardScope | undefined = undefined; @@ -64,11 +63,10 @@ export class AnnotationController extends Disposable { ) { super(() => this.dispose()); - this.onConfigurationChanged(); - this._disposable = Disposable.from( - workspace.onDidChangeConfiguration(this.onConfigurationChanged, this) + configuration.onDidChange(this.onConfigurationChanged, this) ); + this.onConfigurationChanged(configuration.initializingChangeEvent); } dispose() { @@ -81,46 +79,47 @@ export class AnnotationController extends Disposable { this._disposable && this._disposable.dispose(); } - private onConfigurationChanged() { - let changed = false; - - const cfg = workspace.getConfiguration().get(ExtensionKey)!; - const cfgBlameHighlight = cfg.blame.file.lineHighlight; - const cfgChangesHighlight = cfg.recentChanges.file.lineHighlight; - const cfgTheme = cfg.theme.lineHighlight; + private onConfigurationChanged(e: ConfigurationChangeEvent) { + const initializing = configuration.initializing(e); - if (!Objects.areEquivalent(cfgBlameHighlight, this._config && this._config.blame.file.lineHighlight) || - !Objects.areEquivalent(cfgChangesHighlight, this._config && this._config.recentChanges.file.lineHighlight) || - !Objects.areEquivalent(cfgTheme, this._config && this._config.theme.lineHighlight)) { - changed = true; + let cfg: IConfig | undefined; + if (initializing || + configuration.changed(e, configuration.name('blame')('file')('lineHighlight').value) || + configuration.changed(e, configuration.name('theme')('lineHighlight').value)) { Decorations.blameHighlight && Decorations.blameHighlight.dispose(); - if (cfgBlameHighlight.enabled) { + if (cfg === undefined) { + cfg = configuration.get(); + } + const cfgHighlight = cfg.blame.file.lineHighlight; + const cfgTheme = cfg.theme.lineHighlight; + + if (cfgHighlight.enabled) { Decorations.blameHighlight = window.createTextEditorDecorationType({ gutterIconSize: 'contain', isWholeLine: true, overviewRulerLane: OverviewRulerLane.Right, dark: { - backgroundColor: cfgBlameHighlight.locations.includes(LineHighlightLocations.Line) - ? cfgTheme.dark.backgroundColor || themeDefaults.lineHighlight.dark.backgroundColor + backgroundColor: cfgHighlight.locations.includes(LineHighlightLocations.Line) + ? cfgTheme.dark.backgroundColor || configuration.defaults.theme.lineHighlight.dark.backgroundColor : undefined, - gutterIconPath: cfgBlameHighlight.locations.includes(LineHighlightLocations.Gutter) + gutterIconPath: cfgHighlight.locations.includes(LineHighlightLocations.Gutter) ? this.context.asAbsolutePath('images/dark/highlight-gutter.svg') : undefined, - overviewRulerColor: cfgBlameHighlight.locations.includes(LineHighlightLocations.OverviewRuler) - ? cfgTheme.dark.overviewRulerColor || themeDefaults.lineHighlight.dark.overviewRulerColor + overviewRulerColor: cfgHighlight.locations.includes(LineHighlightLocations.OverviewRuler) + ? cfgTheme.dark.overviewRulerColor || configuration.defaults.theme.lineHighlight.dark.overviewRulerColor : undefined }, light: { - backgroundColor: cfgBlameHighlight.locations.includes(LineHighlightLocations.Line) - ? cfgTheme.light.backgroundColor || themeDefaults.lineHighlight.light.backgroundColor + backgroundColor: cfgHighlight.locations.includes(LineHighlightLocations.Line) + ? cfgTheme.light.backgroundColor || configuration.defaults.theme.lineHighlight.light.backgroundColor : undefined, - gutterIconPath: cfgBlameHighlight.locations.includes(LineHighlightLocations.Gutter) + gutterIconPath: cfgHighlight.locations.includes(LineHighlightLocations.Gutter) ? this.context.asAbsolutePath('images/light/highlight-gutter.svg') : undefined, - overviewRulerColor: cfgBlameHighlight.locations.includes(LineHighlightLocations.OverviewRuler) - ? cfgTheme.light.overviewRulerColor || themeDefaults.lineHighlight.light.overviewRulerColor + overviewRulerColor: cfgHighlight.locations.includes(LineHighlightLocations.OverviewRuler) + ? cfgTheme.light.overviewRulerColor || configuration.defaults.theme.lineHighlight.light.overviewRulerColor : undefined } }); @@ -128,48 +127,58 @@ export class AnnotationController extends Disposable { else { Decorations.blameHighlight = undefined; } + } + if (initializing || + configuration.changed(e, configuration.name('recentChanges')('file')('lineHighlight').value) || + configuration.changed(e, configuration.name('theme')('lineHighlight').value)) { Decorations.recentChangesHighlight && Decorations.recentChangesHighlight.dispose(); + if (cfg === undefined) { + cfg = configuration.get(); + } + const cfgHighlight = cfg.recentChanges.file.lineHighlight; + const cfgTheme = cfg.theme.lineHighlight; + Decorations.recentChangesHighlight = window.createTextEditorDecorationType({ gutterIconSize: 'contain', isWholeLine: true, overviewRulerLane: OverviewRulerLane.Right, dark: { - backgroundColor: cfgChangesHighlight.locations.includes(LineHighlightLocations.Line) - ? cfgTheme.dark.backgroundColor || themeDefaults.lineHighlight.dark.backgroundColor + backgroundColor: cfgHighlight.locations.includes(LineHighlightLocations.Line) + ? cfgTheme.dark.backgroundColor || configuration.defaults.theme.lineHighlight.dark.backgroundColor : undefined, - gutterIconPath: cfgChangesHighlight.locations.includes(LineHighlightLocations.Gutter) + gutterIconPath: cfgHighlight.locations.includes(LineHighlightLocations.Gutter) ? this.context.asAbsolutePath('images/dark/highlight-gutter.svg') : undefined, - overviewRulerColor: cfgChangesHighlight.locations.includes(LineHighlightLocations.OverviewRuler) - ? cfgTheme.dark.overviewRulerColor || themeDefaults.lineHighlight.dark.overviewRulerColor + overviewRulerColor: cfgHighlight.locations.includes(LineHighlightLocations.OverviewRuler) + ? cfgTheme.dark.overviewRulerColor || configuration.defaults.theme.lineHighlight.dark.overviewRulerColor : undefined }, light: { - backgroundColor: cfgChangesHighlight.locations.includes(LineHighlightLocations.Line) - ? cfgTheme.light.backgroundColor || themeDefaults.lineHighlight.light.backgroundColor + backgroundColor: cfgHighlight.locations.includes(LineHighlightLocations.Line) + ? cfgTheme.light.backgroundColor || configuration.defaults.theme.lineHighlight.light.backgroundColor : undefined, - gutterIconPath: cfgChangesHighlight.locations.includes(LineHighlightLocations.Gutter) + gutterIconPath: cfgHighlight.locations.includes(LineHighlightLocations.Gutter) ? this.context.asAbsolutePath('images/light/highlight-gutter.svg') : undefined, - overviewRulerColor: cfgChangesHighlight.locations.includes(LineHighlightLocations.OverviewRuler) - ? cfgTheme.light.overviewRulerColor || themeDefaults.lineHighlight.light.overviewRulerColor + overviewRulerColor: cfgHighlight.locations.includes(LineHighlightLocations.OverviewRuler) + ? cfgTheme.light.overviewRulerColor || configuration.defaults.theme.lineHighlight.light.overviewRulerColor : undefined } }); } - if (!Objects.areEquivalent(cfg.blame.file, this._config && this._config.blame.file) || - !Objects.areEquivalent(cfg.recentChanges.file, this._config && this._config.recentChanges.file) || - !Objects.areEquivalent(cfg.annotations, this._config && this._config.annotations) || - !Objects.areEquivalent(cfg.theme.annotations, this._config && this._config.theme.annotations)) { - changed = true; - } + if (initializing) return; - this._config = cfg; + if (configuration.changed(e, configuration.name('blame')('file').value) || + configuration.changed(e, configuration.name('recentChanges')('file').value) || + configuration.changed(e, configuration.name('annotations')('file').value) || + configuration.changed(e, configuration.name('theme')('annotations')('file').value)) { + if (cfg === undefined) { + cfg = configuration.get(); + } - if (changed) { // Since the configuration has changed -- reset any visible annotations for (const provider of this._annotationProviders.values()) { if (provider === undefined) continue; @@ -178,7 +187,7 @@ export class AnnotationController extends Disposable { provider.reset(Decorations.recentChangesAnnotation, Decorations.recentChangesHighlight); } else { - if (provider.annotationType === this._config.blame.file.annotationType) { + if (provider.annotationType === cfg.blame.file.annotationType) { provider.reset(Decorations.blameAnnotation, Decorations.blameHighlight); } else { diff --git a/src/annotations/annotationProvider.ts b/src/annotations/annotationProvider.ts index 3b14724..1924741 100644 --- a/src/annotations/annotationProvider.ts +++ b/src/annotations/annotationProvider.ts @@ -1,8 +1,8 @@ 'use strict'; -import { DecorationOptions, Disposable, ExtensionContext, TextDocument, TextEditor, TextEditorDecorationType, TextEditorSelectionChangeEvent, Uri, window, workspace } from 'vscode'; +import { DecorationOptions, Disposable, ExtensionContext, TextDocument, TextEditor, TextEditorDecorationType, TextEditorSelectionChangeEvent, Uri, window } from 'vscode'; import { FileAnnotationType } from '../annotations/annotationController'; import { TextDocumentComparer } from '../comparers'; -import { ExtensionKey, IConfig } from '../configuration'; +import { configuration, IConfig } from '../configuration'; export type TextEditorCorrelationKey = string; @@ -31,7 +31,7 @@ export abstract class AnnotationProviderBase extends Disposable { this.correlationKey = AnnotationProviderBase.getCorrelationKey(this.editor); this.document = this.editor.document; - this._config = workspace.getConfiguration().get(ExtensionKey)!; + this._config = configuration.get(); this._disposable = Disposable.from( window.onDidChangeTextEditorSelection(this.onTextEditorSelectionChanged, this) @@ -78,7 +78,7 @@ export abstract class AnnotationProviderBase extends Disposable { async reset(decoration: TextEditorDecorationType | undefined, highlightDecoration: TextEditorDecorationType | undefined) { await this.clear(); - this._config = workspace.getConfiguration().get(ExtensionKey)!; + this._config = configuration.get(); this.decoration = decoration; this.highlightDecoration = highlightDecoration; diff --git a/src/annotations/blameAnnotationProvider.ts b/src/annotations/blameAnnotationProvider.ts index 5d8c852..1efcaf8 100644 --- a/src/annotations/blameAnnotationProvider.ts +++ b/src/annotations/blameAnnotationProvider.ts @@ -103,7 +103,7 @@ export abstract class BlameAnnotationProviderBase extends AnnotationProviderBase } } - const message = Annotations.getHoverMessage(logCommit || commit, this._config.defaultDateFormat, this.git.hasRemotes(commit.repoPath), this._config.blame.file.annotationType); + const message = Annotations.getHoverMessage(logCommit || commit, this._config.defaultDateFormat, await this.git.hasRemotes(commit.repoPath), this._config.blame.file.annotationType); return new Hover(message, document.validateRange(new Range(position.line, 0, position.line, endOfLineIndex))); } diff --git a/src/annotations/recentChangesAnnotationProvider.ts b/src/annotations/recentChangesAnnotationProvider.ts index 1835f0a..f283040 100644 --- a/src/annotations/recentChangesAnnotationProvider.ts +++ b/src/annotations/recentChangesAnnotationProvider.ts @@ -48,7 +48,7 @@ export class RecentChangesAnnotationProvider extends AnnotationProviderBase { if (cfg.hover.details) { this._decorations.push({ - hoverMessage: Annotations.getHoverMessage(commit, dateFormat, this.git.hasRemotes(commit.repoPath), this._config.blame.file.annotationType), + hoverMessage: Annotations.getHoverMessage(commit, dateFormat, await this.git.hasRemotes(commit.repoPath), this._config.blame.file.annotationType), range: range } as DecorationOptions); } diff --git a/src/codeLensController.ts b/src/codeLensController.ts index de2508a..188cd01 100644 --- a/src/codeLensController.ts +++ b/src/codeLensController.ts @@ -1,18 +1,17 @@ 'use strict'; -import { Objects } from './system'; -import { Disposable, ExtensionContext, languages, TextEditor, workspace } from 'vscode'; -import { IConfig } from './configuration'; -import { CommandContext, ExtensionKey, setCommandContext } from './constants'; +import { ConfigurationChangeEvent, Disposable, ExtensionContext, languages, TextEditor } from 'vscode'; +import { configuration, ICodeLensConfig } from './configuration'; +import { CommandContext, setCommandContext } from './constants'; import { GitCodeLensProvider } from './gitCodeLensProvider'; import { BlameabilityChangeEvent, BlameabilityChangeReason, GitContextTracker, GitService } from './gitService'; import { Logger } from './logger'; export class CodeLensController extends Disposable { - private _codeLensProvider: GitCodeLensProvider | undefined; - private _codeLensProviderDisposable: Disposable | undefined; - private _config: IConfig; + private _canToggle: boolean; private _disposable: Disposable | undefined; + private _provider: GitCodeLensProvider | undefined; + private _providerDisposable: Disposable | undefined; constructor( private readonly context: ExtensionContext, @@ -21,71 +20,71 @@ export class CodeLensController extends Disposable { ) { super(() => this.dispose()); - this.onConfigurationChanged(); - this._disposable = Disposable.from( - workspace.onDidChangeConfiguration(this.onConfigurationChanged, this), + configuration.onDidChange(this.onConfigurationChanged, this), this.gitContextTracker.onDidChangeBlameability(this.onBlameabilityChanged, this) ); + this.onConfigurationChanged(configuration.initializingChangeEvent); } dispose() { + this._providerDisposable && this._providerDisposable.dispose(); this._disposable && this._disposable.dispose(); - - this._codeLensProviderDisposable && this._codeLensProviderDisposable.dispose(); - this._codeLensProviderDisposable = undefined; - this._codeLensProvider = undefined; } - private onConfigurationChanged() { - const cfg = workspace.getConfiguration().get(ExtensionKey)!; + private onConfigurationChanged(e: ConfigurationChangeEvent) { + const initializing = configuration.initializing(e); - if (!Objects.areEquivalent(cfg.codeLens, this._config && this._config.codeLens)) { - if (this._config !== undefined) { + const section = configuration.name('codeLens').value; + if (initializing || configuration.changed(e, section, null)) { + if (!initializing) { Logger.log('CodeLens config changed; resetting CodeLens provider'); } - if (cfg.codeLens.enabled && (cfg.codeLens.recentChange.enabled || cfg.codeLens.authors.enabled)) { - if (this._codeLensProvider) { - this._codeLensProvider.reset(); + const cfg = configuration.get(section); + if (cfg.enabled && (cfg.recentChange.enabled || cfg.authors.enabled)) { + if (this._provider !== undefined) { + this._provider.reset(); } else { - this._codeLensProvider = new GitCodeLensProvider(this.context, this.git); - this._codeLensProviderDisposable = languages.registerCodeLensProvider(GitCodeLensProvider.selector, this._codeLensProvider); + this._provider = new GitCodeLensProvider(this.context, this.git); + this._providerDisposable = languages.registerCodeLensProvider(GitCodeLensProvider.selector, this._provider); } } else { - this._codeLensProviderDisposable && this._codeLensProviderDisposable.dispose(); - this._codeLensProviderDisposable = undefined; - this._codeLensProvider = undefined; + if (this._providerDisposable !== undefined) { + this._providerDisposable.dispose(); + this._providerDisposable = undefined; + } + this._provider = undefined; } - setCommandContext(CommandContext.CanToggleCodeLens, cfg.codeLens.recentChange.enabled || cfg.codeLens.authors.enabled); + this._canToggle = cfg.recentChange.enabled || cfg.authors.enabled; + setCommandContext(CommandContext.CanToggleCodeLens, this._canToggle); } - - this._config = cfg; } private onBlameabilityChanged(e: BlameabilityChangeEvent) { - if (this._codeLensProvider === undefined) return; + if (this._provider === undefined) return; // Don't reset if this was an editor change, because code lens will naturally be re-rendered if (e.blameable && e.reason !== BlameabilityChangeReason.EditorChanged) { Logger.log('Blameability changed; resetting CodeLens provider'); - this._codeLensProvider.reset(); + this._provider.reset(); } } toggleCodeLens(editor: TextEditor) { - if (!this._config.codeLens.recentChange.enabled && !this._config.codeLens.authors.enabled) return; + if (!this._canToggle) return; Logger.log(`toggleCodeLens()`); - if (this._codeLensProviderDisposable) { - this._codeLensProviderDisposable.dispose(); - this._codeLensProviderDisposable = undefined; + if (this._providerDisposable !== undefined) { + this._providerDisposable.dispose(); + this._providerDisposable = undefined; + return; } - this._codeLensProviderDisposable = languages.registerCodeLensProvider(GitCodeLensProvider.selector, new GitCodeLensProvider(this.context, this.git)); + this._providerDisposable = languages.registerCodeLensProvider(GitCodeLensProvider.selector, new GitCodeLensProvider(this.context, this.git)); } } diff --git a/src/configuration.ts b/src/configuration.ts index f888042..f30ba71 100644 --- a/src/configuration.ts +++ b/src/configuration.ts @@ -1,10 +1,13 @@ 'use strict'; +import { ConfigurationChangeEvent, Event, EventEmitter, ExtensionContext, Uri, workspace } from 'vscode'; import { FileAnnotationType } from './annotations/annotationController'; +import { ExtensionKey } from './constants'; import { LineAnnotationType } from './currentLineController'; import { GitExplorerView } from './views/gitExplorer'; import { OutputLevel } from './logger'; +import { Functions } from './system'; -export { ExtensionKey } from './constants'; +export { ExtensionKey }; export enum CodeLensCommand { DiffWithPrevious = 'gitlens.diffWithPrevious', @@ -95,6 +98,22 @@ export interface IAdvancedConfig { }; } +export interface ICodeLensConfig { + enabled: boolean; + recentChange: { + enabled: boolean; + command: CodeLensCommand; + }; + authors: { + enabled: boolean; + command: CodeLensCommand; + }; + locations: CodeLensLocations[]; + customLocationSymbols: string[]; + perLanguageLocations: ICodeLensLanguageLocation[]; + debug: boolean; +} + export interface ICodeLensLanguageLocation { language: string | undefined; locations: CodeLensLocations[]; @@ -124,17 +143,19 @@ export interface IRemotesConfig { type: CustomRemoteType; domain: string; name?: string; - urls?: { - repository: string; - branches: string; - branch: string; - commit: string; - file: string; - fileInBranch: string; - fileInCommit: string; - fileLine: string; - fileRange: string; - }; + urls?: IRemotesUrlsConfig; +} + +export interface IRemotesUrlsConfig { + repository: string; + branches: string; + branch: string; + commit: string; + file: string; + fileInBranch: string; + fileInCommit: string; + fileLine: string; + fileRange: string; } export interface IThemeConfig { @@ -300,21 +321,7 @@ export interface IConfig { } }; - codeLens: { - enabled: boolean; - recentChange: { - enabled: boolean; - command: CodeLensCommand; - }; - authors: { - enabled: boolean; - command: CodeLensCommand; - }; - locations: CodeLensLocations[]; - customLocationSymbols: string[]; - perLanguageLocations: ICodeLensLanguageLocation[]; - debug: boolean; - }; + codeLens: ICodeLensConfig; defaultDateFormat: string | null; @@ -347,4 +354,212 @@ export interface IConfig { outputLevel: OutputLevel; advanced: IAdvancedConfig; -} \ No newline at end of file +} + +const emptyConfig: IConfig = { + annotations: { + file: { + gutter: { + format: '', + dateFormat: null, + compact: false, + heatmap: { + enabled: false, + location: 'left' + }, + hover: { + details: false, + changes: false, + wholeLine: false + } + }, + hover: { + details: false, + changes: false, + heatmap: { + enabled: false + } + }, + recentChanges: { + hover: { + details: false, + changes: false + } + } + }, + line: { + hover: { + details: false, + changes: false + }, + trailing: { + format: '', + dateFormat: null, + hover: { + details: false, + changes: false, + wholeLine: false + } + } + } + }, + blame: { + ignoreWhitespace: false, + file: { + annotationType: 'gutter' as FileAnnotationType, + lineHighlight: { + enabled: false, + locations: [] + } + }, + line: { + enabled: false, + annotationType: 'trailing' as LineAnnotationType + } + }, + recentChanges: { + file: { + lineHighlight: { + locations: [] + } + } + }, + codeLens: { + enabled: false, + recentChange: { + enabled: false, + command: CodeLensCommand.DiffWithPrevious + }, + authors: { + enabled: false, + command: CodeLensCommand.DiffWithPrevious + }, + locations: [], + customLocationSymbols: [], + perLanguageLocations: [], + debug: false + }, + defaultDateFormat: null, + gitExplorer: { + enabled: false, + autoRefresh: false, + view: GitExplorerView.Auto, + files: { + layout: GitExplorerFilesLayout.Auto, + compact: false, + threshold: 0 + }, + includeWorkingTree: false, + showTrackingBranch: false, + commitFormat: '', + commitFileFormat: '', + stashFormat: '', + stashFileFormat: '', + statusFileFormat: '' + // dateFormat: string | null; + }, + remotes: [], + statusBar: { + enabled: false, + alignment: 'left', + command: StatusBarCommand.DiffWithPrevious, + format: '', + dateFormat: null + }, + strings: { + codeLens: { + unsavedChanges: { + recentChangeAndAuthors: '', + recentChangeOnly: '', + authorsOnly: '' + } + } + }, + theme: themeDefaults, + debug: false, + insiders: false, + outputLevel: 'verbose' as OutputLevel, + advanced: { + caching: { + enabled: false, + maxLines: 0 + }, + git: '', + maxQuickHistory: 0, + menus: { + explorerContext: { + fileDiff: false, + history: false, + remote: false + }, + editorContext: { + blame: false, + copy: false, + details: false, + fileDiff: false, + history: false, + lineDiff: false, + remote: false + }, + editorTitle: { + blame: false, + fileDiff: false, + history: false, + status: false + }, + editorTitleContext: { + blame: false, + fileDiff: false, + history: false, + remote: false + } + }, + quickPick: { + closeOnFocusOut: false + }, + telemetry: { + enabled: false + } + } +}; + +export class Configuration { + + static configure(context: ExtensionContext) { + context.subscriptions.push(workspace.onDidChangeConfiguration(configuration.onConfigurationChanged, configuration)); + } + + private _onDidChange = new EventEmitter(); + get onDidChange(): Event { + return this._onDidChange.event; + } + + private onConfigurationChanged(e: ConfigurationChangeEvent) { + if (!e.affectsConfiguration(ExtensionKey, null!)) return; + + this._onDidChange.fire(e); + } + + readonly defaults = { theme: themeDefaults }; + readonly initializingChangeEvent: ConfigurationChangeEvent = { + affectsConfiguration: (section: string, resource?: Uri) => false + }; + + get(section?: string, resource?: Uri | null) { + return workspace.getConfiguration(section === undefined ? undefined : ExtensionKey, resource!).get(section === undefined ? ExtensionKey : section)!; + } + + changed(e: ConfigurationChangeEvent, section: string, resource?: Uri | null) { + return e.affectsConfiguration(`${ExtensionKey}.${section}`, resource!); + } + + initializing(e: ConfigurationChangeEvent) { + return e === this.initializingChangeEvent; + } + + name(name: K) { + return Functions.propOf(emptyConfig, name); + } +} + +export const configuration = new Configuration(); \ No newline at end of file diff --git a/src/currentLineController.ts b/src/currentLineController.ts index 37bc4da..27430da 100644 --- a/src/currentLineController.ts +++ b/src/currentLineController.ts @@ -1,12 +1,12 @@ 'use strict'; -import { Functions, Objects } from './system'; -import { CancellationToken, debug, DecorationRangeBehavior, DecorationRenderOptions, Disposable, ExtensionContext, Hover, HoverProvider, languages, Position, Range, StatusBarAlignment, StatusBarItem, TextDocument, TextEditor, TextEditorDecorationType, TextEditorSelectionChangeEvent, window, workspace } from 'vscode'; +import { Functions } from './system'; +import { CancellationToken, ConfigurationChangeEvent, debug, DecorationRangeBehavior, DecorationRenderOptions, Disposable, ExtensionContext, Hover, HoverProvider, languages, Position, Range, StatusBarAlignment, StatusBarItem, TextDocument, TextEditor, TextEditorDecorationType, TextEditorSelectionChangeEvent, window } from 'vscode'; import { AnnotationController, FileAnnotationType } from './annotations/annotationController'; import { Annotations, endOfLineIndex } from './annotations/annotations'; import { Commands } from './commands'; import { TextEditorComparer } from './comparers'; -import { IConfig, StatusBarCommand } from './configuration'; -import { DocumentSchemes, ExtensionKey, isTextEditor } from './constants'; +import { configuration, IConfig, StatusBarCommand } from './configuration'; +import { DocumentSchemes, isTextEditor } from './constants'; import { BlameabilityChangeEvent, CommitFormatter, GitCommit, GitCommitLine, GitContextTracker, GitLogCommit, GitService, GitUri, ICommitFormatOptions } from './gitService'; // import { Logger } from './logger'; @@ -26,7 +26,7 @@ export enum LineAnnotationType { export class CurrentLineController extends Disposable { private _blameable: boolean; - private _blameLineAnnotationState: { enabled: boolean, annotationType: LineAnnotationType, reason: 'user' | 'debugging' } | undefined = undefined; + private _blameLineAnnotationState: { enabled: boolean, annotationType: LineAnnotationType, reason: 'user' | 'debugging' } | undefined; private _config: IConfig; private _currentLine: { line: number, commit?: GitCommit, logCommit?: GitLogCommit } = { line: -1 }; private _debugSessionEndDisposable: Disposable | undefined; @@ -49,13 +49,12 @@ export class CurrentLineController extends Disposable { this._updateBlameDebounced = Functions.debounce(this.updateBlame, 250); - this.onConfigurationChanged(); - this._disposable = Disposable.from( - workspace.onDidChangeConfiguration(this.onConfigurationChanged, this), + configuration.onDidChange(this.onConfigurationChanged, this), annotationController.onDidToggleAnnotations(this.onFileAnnotationsToggled, this), debug.onDidStartDebugSession(this.onDebugSessionStarted, this) ); + this.onConfigurationChanged(configuration.initializingChangeEvent); } dispose() { @@ -68,27 +67,28 @@ export class CurrentLineController extends Disposable { this._disposable && this._disposable.dispose(); } - private onConfigurationChanged() { - const cfg = workspace.getConfiguration().get(ExtensionKey)!; + private onConfigurationChanged(e: ConfigurationChangeEvent) { + const initializing = configuration.initializing(e); + + const cfg = configuration.get(); let changed = false; - let clear = false; - if (!Objects.areEquivalent(cfg.blame.line, this._config && this._config.blame.line)) { + if (initializing || configuration.changed(e, configuration.name('blame')('line').value)) { changed = true; this._blameLineAnnotationState = undefined; - clear = (this._config !== undefined); } - if (!Objects.areEquivalent(cfg.annotations.line.trailing, this._config && this._config.annotations.line.trailing) || - !Objects.areEquivalent(cfg.annotations.line.hover, this._config && this._config.annotations.line.hover) || - !Objects.areEquivalent(cfg.theme.annotations.line.trailing, this._config && this._config.theme.annotations.line.trailing)) { + if (initializing || + configuration.changed(e, configuration.name('annotations')('line')('trailing').value) || + configuration.changed(e, configuration.name('annotations')('line')('hover').value) || + configuration.changed(e, configuration.name('theme')('annotations')('line')('trailing').value)) { changed = true; - clear = (this._config !== undefined); } - if (!Objects.areEquivalent(cfg.statusBar, this._config && this._config.statusBar)) { + if (initializing || configuration.changed(e, configuration.name('statusBar').value)) { changed = true; + if (cfg.statusBar.enabled) { const alignment = cfg.statusBar.alignment !== 'left' ? StatusBarAlignment.Right : StatusBarAlignment.Left; if (this._statusBarItem !== undefined && this._statusBarItem.alignment !== alignment) { @@ -99,7 +99,7 @@ export class CurrentLineController extends Disposable { this._statusBarItem = this._statusBarItem || window.createStatusBarItem(alignment, alignment === StatusBarAlignment.Right ? 1000 : 0); this._statusBarItem.command = cfg.statusBar.command; } - else if (!cfg.statusBar.enabled && this._statusBarItem) { + else if (this._statusBarItem !== undefined) { this._statusBarItem.dispose(); this._statusBarItem = undefined; } @@ -109,19 +109,18 @@ export class CurrentLineController extends Disposable { if (!changed) return; - if (clear) { - this.clearAnnotations(this._editor); - } + const trackCurrentLine = cfg.statusBar.enabled || + cfg.blame.line.enabled || + (this._blameLineAnnotationState !== undefined && this._blameLineAnnotationState.enabled); - const trackCurrentLine = cfg.statusBar.enabled || cfg.blame.line.enabled || (this._blameLineAnnotationState !== undefined && this._blameLineAnnotationState.enabled); - if (trackCurrentLine && !this._trackCurrentLineDisposable) { - this._trackCurrentLineDisposable = Disposable.from( + if (trackCurrentLine) { + this._trackCurrentLineDisposable = this._trackCurrentLineDisposable || Disposable.from( window.onDidChangeActiveTextEditor(Functions.debounce(this.onActiveTextEditorChanged, 50), this), window.onDidChangeTextEditorSelection(this.onTextEditorSelectionChanged, this), this.gitContextTracker.onDidChangeBlameability(this.onBlameabilityChanged, this) ); } - else if (!trackCurrentLine && this._trackCurrentLineDisposable) { + else if (this._trackCurrentLineDisposable !== undefined) { this._trackCurrentLineDisposable.dispose(); this._trackCurrentLineDisposable = undefined; } @@ -251,6 +250,9 @@ export class CurrentLineController extends Disposable { async refresh(editor?: TextEditor) { this._currentLine.line = -1; + + if (editor === undefined && this._editor !== undefined) return; + this.clearAnnotations(this._editor); if (editor === undefined || !this.isEditorBlameable(editor)) { @@ -425,7 +427,7 @@ export class CurrentLineController extends Disposable { } } - const message = Annotations.getHoverMessage(logCommit || commit, this._config.defaultDateFormat, this.git.hasRemotes(commit.repoPath), this._config.blame.file.annotationType); + const message = Annotations.getHoverMessage(logCommit || commit, this._config.defaultDateFormat, await this.git.hasRemotes(commit.repoPath), this._config.blame.file.annotationType); return new Hover(message, range); } diff --git a/src/extension.ts b/src/extension.ts index 9ff4c50..26d07d5 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -2,7 +2,7 @@ // import { Objects } from './system'; import { commands, ExtensionContext, extensions, languages, window, workspace } from 'vscode'; import { AnnotationController } from './annotations/annotationController'; -import { CodeLensLocations, IConfig, LineHighlightLocations } from './configuration'; +import { CodeLensLocations, Configuration, IConfig, LineHighlightLocations } from './configuration'; import { ApplicationInsightsKey, CommandContext, ExtensionKey, GlobalState, QualifiedExtensionId, setCommandContext } from './constants'; import { CodeLensController } from './codeLensController'; import { configureCommands } from './commands'; @@ -10,7 +10,7 @@ import { CurrentLineController, LineAnnotationType } from './currentLineControll import { GitContentProvider } from './gitContentProvider'; import { GitExplorer } from './views/gitExplorer'; import { GitRevisionCodeLensProvider } from './gitRevisionCodeLensProvider'; -import { GitContextTracker, GitService, RemoteProviderFactory } from './gitService'; +import { GitContextTracker, GitService } from './gitService'; import { Keyboard } from './keyboard'; import { Logger } from './logger'; import { Messages, SuppressedKeys } from './messages'; @@ -18,10 +18,10 @@ import { Telemetry } from './telemetry'; // this method is called when your extension is activated export async function activate(context: ExtensionContext) { + Configuration.configure(context); Logger.configure(context); Messages.configure(context); Telemetry.configure(ApplicationInsightsKey); - RemoteProviderFactory.configure(context); const gitlens = extensions.getExtension(QualifiedExtensionId)!; const gitlensVersion = gitlens.packageJSON.version; diff --git a/src/git/git.ts b/src/git/git.ts index 87503fb..94360dc 100644 --- a/src/git/git.ts +++ b/src/git/git.ts @@ -26,11 +26,6 @@ const defaultBlameParams = [`blame`, `--root`, `--incremental`]; const defaultLogParams = [`log`, `--name-status`, `--full-history`, `-M`, `--format=%H -%nauthor %an%nauthor-date %at%nparents %P%nsummary %B%nfilename ?`]; const defaultStashParams = [`stash`, `list`, `--name-status`, `--full-history`, `-M`, `--format=%H -%nauthor-date %at%nreflog-selector %gd%nsummary %B%nfilename ?`]; -let defaultEncoding = 'utf8'; -export function setDefaultEncoding(encoding: string) { - defaultEncoding = iconv.encodingExists(encoding) ? encoding : 'utf8'; -} - const GitWarnings = [ /Not a git repository/, /is outside repository/, @@ -123,6 +118,12 @@ export class Git { return git; } + static getEncoding(encoding: string | undefined) { + return (encoding !== undefined && iconv.encodingExists(encoding)) + ? encoding + : 'utf8'; + } + static async getGitPath(gitPath?: string): Promise { git = await findGitPath(gitPath); Logger.log(`Git found: ${git.version} @ ${git.path === 'git' ? 'PATH' : git.path}`); @@ -130,7 +131,7 @@ export class Git { } static async getVersionedFile(repoPath: string | undefined, fileName: string, branchOrSha: string) { - const data = await Git.show(repoPath, fileName, branchOrSha, 'binary'); + const data = await Git.show(repoPath, fileName, branchOrSha, { encoding: 'binary' }); if (data === undefined) return undefined; const suffix = Strings.truncate(Strings.sanitizeForFS(Git.isSha(branchOrSha) ? Git.shortenSha(branchOrSha) : branchOrSha), 50, ''); @@ -243,7 +244,7 @@ export class Git { } } - static diff(repoPath: string, fileName: string, sha1?: string, sha2?: string, encoding?: string) { + static diff(repoPath: string, fileName: string, sha1?: string, sha2?: string, options: { encoding?: string } = {}) { const params = [`diff`, `--diff-filter=M`, `-M`, `--no-ext-diff`]; if (sha1) { params.push(sha1); @@ -252,7 +253,7 @@ export class Git { params.push(sha2); } - return gitCommand({ cwd: repoPath, encoding: encoding || defaultEncoding }, ...params, '--', fileName); + return gitCommand({ cwd: repoPath, encoding: options.encoding || 'utf8' }, ...params, '--', fileName); } static diff_nameStatus(repoPath: string, sha1?: string, sha2?: string, options: { filter?: string } = {}) { @@ -420,11 +421,11 @@ export class Git { } } - static async show(repoPath: string | undefined, fileName: string, branchOrSha: string, encoding?: string) { + static async show(repoPath: string | undefined, fileName: string, branchOrSha: string, options: { encoding?: string } = {}) { const [file, root] = Git.splitPath(fileName, repoPath); if (Git.isUncommitted(branchOrSha)) throw new Error(`sha=${branchOrSha} is uncommitted`); - const opts = { cwd: root, encoding: encoding || defaultEncoding, willHandleErrors: true } as GitCommandOptions; + const opts = { cwd: root, encoding: options.encoding || 'utf8', willHandleErrors: true } as GitCommandOptions; const args = `${branchOrSha}:./${file}`; try { const data = await gitCommand(opts, 'show', args); diff --git a/src/git/gitContextTracker.ts b/src/git/gitContextTracker.ts index 3aaca09..4909377 100644 --- a/src/git/gitContextTracker.ts +++ b/src/git/gitContextTracker.ts @@ -1,7 +1,8 @@ 'use strict'; import { Functions } from '../system'; -import { Disposable, Event, EventEmitter, TextDocumentChangeEvent, TextEditor, window, workspace } from 'vscode'; +import { ConfigurationChangeEvent, Disposable, Event, EventEmitter, TextDocumentChangeEvent, TextEditor, window, workspace } from 'vscode'; import { TextDocumentComparer } from '../comparers'; +import { configuration } from '../configuration'; import { CommandContext, isTextEditor, setCommandContext } from '../constants'; import { GitChangeEvent, GitChangeReason, GitService, GitUri, Repository, RepositoryChangeEvent } from '../gitService'; // import { Logger } from '../logger'; @@ -40,45 +41,49 @@ export class GitContextTracker extends Disposable { return this._onDidChangeBlameability.event; } - private _context: Context = { state: { dirty: false } }; - private _disposable: Disposable | undefined; - private _gitEnabled: boolean; + private readonly _context: Context = { state: { dirty: false } }; + private readonly _disposable: Disposable; + private _listenersDisposable: Disposable | undefined; constructor( private readonly git: GitService ) { super(() => this.dispose()); - this.onConfigurationChanged(); + this._disposable = Disposable.from( + workspace.onDidChangeConfiguration(this.onConfigurationChanged, this) + ); + this.onConfigurationChanged(configuration.initializingChangeEvent); } dispose() { + this._listenersDisposable && this._listenersDisposable.dispose(); this._disposable && this._disposable.dispose(); } - private onConfigurationChanged() { - const gitEnabled = workspace.getConfiguration('git').get('enabled', true); - if (this._gitEnabled !== gitEnabled) { - this._gitEnabled = gitEnabled; + private onConfigurationChanged(e: ConfigurationChangeEvent) { + if (!configuration.initializing(e) && !e.affectsConfiguration('git.enabled', null!)) return; - if (this._disposable !== undefined) { - this._disposable.dispose(); - this._disposable = undefined; - } + const enabled = workspace.getConfiguration('git', null!).get('enabled', true); + if (this._listenersDisposable !== undefined) { + this._listenersDisposable.dispose(); + this._listenersDisposable = undefined; + } - setCommandContext(CommandContext.Enabled, gitEnabled); + setCommandContext(CommandContext.Enabled, enabled); - if (gitEnabled) { - this._disposable = Disposable.from( - window.onDidChangeActiveTextEditor(Functions.debounce(this.onActiveTextEditorChanged, 50), this), - workspace.onDidChangeConfiguration(this.onConfigurationChanged, this), - workspace.onDidChangeTextDocument(Functions.debounce(this.onTextDocumentChanged, 50), this), - this.git.onDidBlameFail(this.onBlameFailed, this), - this.git.onDidChange(this.onGitChanged, this) - ); + if (enabled) { + this._listenersDisposable = Disposable.from( + window.onDidChangeActiveTextEditor(Functions.debounce(this.onActiveTextEditorChanged, 50), this), + workspace.onDidChangeTextDocument(Functions.debounce(this.onTextDocumentChanged, 50), this), + this.git.onDidBlameFail(this.onBlameFailed, this), + this.git.onDidChange(this.onGitChanged, this) + ); - this.updateContext(BlameabilityChangeReason.EditorChanged, window.activeTextEditor, true); - } + this.updateContext(BlameabilityChangeReason.EditorChanged, window.activeTextEditor, true); + } + else { + this.updateContext(BlameabilityChangeReason.EditorChanged, window.activeTextEditor, false); } } @@ -98,9 +103,9 @@ export class GitContextTracker extends Disposable { } private onGitChanged(e: GitChangeEvent) { - if (e.reason === GitChangeReason.RemoteCache || e.reason === GitChangeReason.Repositories) { - this.updateRemotes(); - } + if (e.reason !== GitChangeReason.Repositories) return; + + this.updateRemotes(); } private onRepoChanged(e: RepositoryChangeEvent) { @@ -186,9 +191,7 @@ export class GitContextTracker extends Disposable { private async updateRemotes() { let hasRemotes = false; if (this._context.repo !== undefined) { - const remotes = await this.git.getRemotes(this._context.repo.path); - - hasRemotes = remotes.length !== 0; + hasRemotes = await this._context.repo.hasRemotes(); setCommandContext(CommandContext.ActiveHasRemotes, hasRemotes); } else { @@ -200,9 +203,7 @@ export class GitContextTracker extends Disposable { for (const repo of repositories) { if (repo === this._context.repo) continue; - const remotes = await this.git.getRemotes(repo.path); - hasRemotes = remotes.length !== 0; - + hasRemotes = await repo.hasRemotes(); if (hasRemotes) break; } } diff --git a/src/git/models/remote.ts b/src/git/models/remote.ts index 03376b4..5e56015 100644 --- a/src/git/models/remote.ts +++ b/src/git/models/remote.ts @@ -1,5 +1,5 @@ 'use strict'; -import { RemoteProvider, RemoteProviderFactory } from '../remotes/factory'; +import { RemoteProvider } from '../remotes/factory'; export enum GitRemoteType { Fetch = 'fetch', @@ -8,15 +8,12 @@ export enum GitRemoteType { export class GitRemote { - provider?: RemoteProvider; - constructor( public readonly repoPath: string, public readonly name: string, public readonly domain: string, public readonly path: string, + public readonly provider: RemoteProvider | undefined, public readonly types: { type: GitRemoteType, url: string }[] - ) { - this.provider = RemoteProviderFactory.getRemoteProvider(this.domain, this.path); - } + ) { } } \ No newline at end of file diff --git a/src/git/models/repository.ts b/src/git/models/repository.ts index 11282b4..e3f4ee4 100644 --- a/src/git/models/repository.ts +++ b/src/git/models/repository.ts @@ -1,9 +1,14 @@ 'use strict'; import { Functions } from '../../system'; -import { Disposable, Event, EventEmitter, RelativePattern, Uri, workspace, WorkspaceFolder } from 'vscode'; +import { ConfigurationChangeEvent, Disposable, Event, EventEmitter, RelativePattern, Uri, workspace, WorkspaceFolder } from 'vscode'; +import { configuration, IRemotesConfig } from '../../configuration'; +import { GitBranch, GitDiffShortStat, GitRemote, GitStash, GitStatus } from '../git'; +import { GitService, GitUri } from '../../gitService'; +import { RemoteProviderFactory, RemoteProviderMap } from '../remotes/factory'; export enum RepositoryChange { // FileSystem = 'file-system', + RemotesCache = 'remotes-cache', Repository = 'repository', Stashes = 'stashes' } @@ -55,6 +60,7 @@ export class Repository extends Disposable { readonly index: number; readonly name: string; + readonly normalizedPath: string; readonly storage: Map = new Map(); private readonly _disposable: Disposable; @@ -63,11 +69,14 @@ export class Repository extends Disposable { private _fsWatchCounter = 0; private _fsWatcherDisposable: Disposable | undefined; private _pendingChanges: { repo?: RepositoryChangeEvent, fs?: RepositoryFileSystemChangeEvent } = { }; + private _providerMap: RemoteProviderMap; + private _remotes: GitRemote[] | undefined; private _suspended: boolean; constructor( private readonly folder: WorkspaceFolder, public readonly path: string, + private readonly git: GitService, private readonly onAnyRepositoryChanged: () => void, suspended: boolean ) { @@ -75,6 +84,8 @@ export class Repository extends Disposable { this.index = folder.index; this.name = folder.name; + this.normalizedPath = (this.path.endsWith('/') ? this.path : `${this.path}/`).toLowerCase(); + this._suspended = suspended; const watcher = workspace.createFileSystemWatcher(new RelativePattern(folder, '**/.git/{index,HEAD,refs/stash,refs/heads/**,refs/remotes/**}')); @@ -82,8 +93,10 @@ export class Repository extends Disposable { watcher, watcher.onDidChange(this.onRepositoryChanged, this), watcher.onDidCreate(this.onRepositoryChanged, this), - watcher.onDidDelete(this.onRepositoryChanged, this) + watcher.onDidDelete(this.onRepositoryChanged, this), + configuration.onDidChange(this.onConfigurationChanged, this) ); + this.onConfigurationChanged(configuration.initializingChangeEvent); } dispose() { @@ -99,6 +112,20 @@ export class Repository extends Disposable { this._disposable && this._disposable.dispose(); } + private onConfigurationChanged(e: ConfigurationChangeEvent) { + const initializing = configuration.initializing(e); + + const section = configuration.name('remotes').value; + if (initializing || configuration.changed(e, section, this.folder.uri)) { + this._providerMap = RemoteProviderFactory.createMap(configuration.get(section, this.folder.uri)); + + if (!initializing) { + this._remotes = undefined; + this.fireChange(RepositoryChange.RemotesCache); + } + } + } + private onFileSystemChanged(uri: Uri) { // Ignore .git changes if (/\.git/.test(uri.fsPath)) return; @@ -177,6 +204,39 @@ export class Repository extends Disposable { return this.folder === workspace.getWorkspaceFolder(uri); } + async getBranch(): Promise { + return this.git.getBranch(this.path); + } + + async getBranches(): Promise { + return this.git.getBranches(this.path); + } + + async getChangedFilesCount(sha?: string): Promise { + return this.git.getChangedFilesCount(this.path, sha); + } + + async getRemotes(): Promise { + if (this._remotes === undefined) { + this._remotes = await this.git.getRemotesCore(this.path, this._providerMap); + } + + return this._remotes; + } + + async getStashList(): Promise { + return this.git.getStashList(this.path); + } + + async getStatus(): Promise { + return this.git.getStatusForRepo(this.path); + } + + async hasRemotes(): Promise { + const remotes = await this.getRemotes(); + return remotes !== undefined && remotes.length > 0; + } + resume() { if (!this._suspended) return; diff --git a/src/git/parsers/remoteParser.ts b/src/git/parsers/remoteParser.ts index 45b5fcc..2935971 100644 --- a/src/git/parsers/remoteParser.ts +++ b/src/git/parsers/remoteParser.ts @@ -1,13 +1,14 @@ 'use strict'; import { GitRemote } from './../git'; import { GitRemoteType } from '../models/remote'; +import { RemoteProvider } from '../remotes/factory'; const remoteRegex = /^(.*)\t(.*)\s\((.*)\)$/gm; const urlRegex = /^(?:git:\/\/(.*?)\/|https:\/\/(.*?)\/|http:\/\/(.*?)\/|git@(.*):|ssh:\/\/(?:.*@)?(.*?)(?::.*?)?\/)(.*)$/; export class GitRemoteParser { - static parse(data: string, repoPath: string): GitRemote[] { + static parse(data: string, repoPath: string, providerFactory: (domain: string, path: string) => RemoteProvider | undefined): GitRemote[] { if (!data) return []; const remotes: GitRemote[] = []; @@ -25,7 +26,7 @@ export class GitRemoteParser { const uniqueness = `${domain}/${path}`; let remote: GitRemote | undefined = groups[uniqueness]; if (remote === undefined) { - remote = new GitRemote(repoPath, match[1], domain, path, [{ url: url, type: match[3] as GitRemoteType }]); + remote = new GitRemote(repoPath, match[1], domain, path, providerFactory(domain, path), [{ url: url, type: match[3] as GitRemoteType }]); remotes.push(remote); groups[uniqueness] = remote; } diff --git a/src/git/remotes/custom.ts b/src/git/remotes/custom.ts index 6f5c2ce..662a7d1 100644 --- a/src/git/remotes/custom.ts +++ b/src/git/remotes/custom.ts @@ -1,17 +1,20 @@ 'use strict'; import { Strings } from '../../system'; import { Range } from 'vscode'; -import { IRemotesConfig } from '../../configuration'; +import { IRemotesConfig, IRemotesUrlsConfig } from '../../configuration'; import { RemoteProvider } from './provider'; export class CustomService extends RemoteProvider { + private readonly urls: IRemotesUrlsConfig; + constructor( domain: string, path: string, - private readonly config: IRemotesConfig + config: IRemotesConfig ) { super(domain, path, config.name, true); + this.urls = config.urls!; } get name() { @@ -19,34 +22,34 @@ export class CustomService extends RemoteProvider { } protected getUrlForRepository(): string { - return Strings.interpolate(this.config.urls!.repository, { repo: this.path }); + return Strings.interpolate(this.urls.repository, { repo: this.path }); } protected getUrlForBranches(): string { - return Strings.interpolate(this.config.urls!.branches, { repo: this.path }); + return Strings.interpolate(this.urls.branches, { repo: this.path }); } protected getUrlForBranch(branch: string): string { - return Strings.interpolate(this.config.urls!.branch, { repo: this.path, branch: branch }); + return Strings.interpolate(this.urls.branch, { repo: this.path, branch: branch }); } protected getUrlForCommit(sha: string): string { - return Strings.interpolate(this.config.urls!.commit, { repo: this.path, id: sha }); + return Strings.interpolate(this.urls.commit, { repo: this.path, id: sha }); } protected getUrlForFile(fileName: string, branch?: string, sha?: string, range?: Range): string { let line = ''; if (range) { if (range.start.line === range.end.line) { - line = Strings.interpolate(this.config.urls!.fileLine, { line: range.start.line }); + line = Strings.interpolate(this.urls.fileLine, { line: range.start.line }); } else { - line = Strings.interpolate(this.config.urls!.fileRange, { start: range.start.line, end: range.end.line }); + line = Strings.interpolate(this.urls.fileRange, { start: range.start.line, end: range.end.line }); } } - if (sha) return Strings.interpolate(this.config.urls!.fileInCommit, { repo: this.path, id: sha, file: fileName, line: line }); - if (branch) return Strings.interpolate(this.config.urls!.fileInBranch, { repo: this.path, branch: branch, file: fileName, line: line }); - return Strings.interpolate(this.config.urls!.file, { repo: this.path, file: fileName, line: line }); + if (sha) return Strings.interpolate(this.urls.fileInCommit, { repo: this.path, id: sha, file: fileName, line: line }); + if (branch) return Strings.interpolate(this.urls.fileInBranch, { repo: this.path, branch: branch, file: fileName, line: line }); + return Strings.interpolate(this.urls.file, { repo: this.path, file: fileName, line: line }); } } \ No newline at end of file diff --git a/src/git/remotes/factory.ts b/src/git/remotes/factory.ts index bd73a65..9595212 100644 --- a/src/git/remotes/factory.ts +++ b/src/git/remotes/factory.ts @@ -1,10 +1,7 @@ 'use strict'; -import { Objects } from '../../system'; -import { Event, EventEmitter, ExtensionContext, workspace } from 'vscode'; import { BitbucketService } from './bitbucket'; import { BitbucketServerService } from './bitbucket-server'; -import { CustomRemoteType, IConfig, IRemotesConfig } from '../../configuration'; -import { ExtensionKey } from '../../constants'; +import { CustomRemoteType, IRemotesConfig } from '../../configuration'; import { CustomService } from './custom'; import { GitHubService } from './github'; import { GitLabService } from './gitlab'; @@ -21,29 +18,22 @@ const defaultProviderMap = new Map Rem ['visualstudio.com', (domain: string, path: string) => new VisualStudioService(domain, path)] ]); -export class RemoteProviderFactory { - - private static _providerMap: Map RemoteProvider>; - private static _remotesCfg: IRemotesConfig[]; +export type RemoteProviderMap = Map RemoteProvider>; - private static _onDidChange = new EventEmitter(); - public static get onDidChange(): Event { - return this._onDidChange.event; - } +export class RemoteProviderFactory { - static configure(context: ExtensionContext) { - context.subscriptions.push(workspace.onDidChangeConfiguration(() => this.onConfigurationChanged())); - this.onConfigurationChanged(true); + static factory(providerMap: RemoteProviderMap): (domain: string, path: string) => RemoteProvider | undefined { + return (domain: string, path: string) => this.create(providerMap, domain, path); } - static getRemoteProvider(domain: string, path: string): RemoteProvider | undefined { + static create(providerMap: RemoteProviderMap, domain: string, path: string): RemoteProvider | undefined { try { let key = domain.toLowerCase(); if (key.endsWith('visualstudio.com')) { key = 'visualstudio.com'; } - const creator = this._providerMap.get(key); + const creator = providerMap.get(key); if (creator === undefined) return undefined; return creator(domain, path); @@ -54,27 +44,17 @@ export class RemoteProviderFactory { } } - private static onConfigurationChanged(silent: boolean = false) { - const cfg = workspace.getConfiguration().get(ExtensionKey); - if (cfg === undefined) return; - - if (!Objects.areEquivalent(cfg.remotes, this._remotesCfg)) { - this._providerMap = new Map(defaultProviderMap); - - this._remotesCfg = cfg.remotes; - if (this._remotesCfg != null && this._remotesCfg.length > 0) { - for (const remoteCfg of this._remotesCfg) { - const provider = this.getCustomProvider(remoteCfg); - if (provider === undefined) continue; - - this._providerMap.set(remoteCfg.domain.toLowerCase(), provider); - } + static createMap(cfg: IRemotesConfig[] | null | undefined): RemoteProviderMap { + const providerMap = new Map(defaultProviderMap); + if (cfg != null && cfg.length > 0) { + for (const rc of cfg) { + const provider = this.getCustomProvider(rc); + if (provider === undefined) continue; - if (!silent) { - this._onDidChange.fire(); - } + providerMap.set(rc.domain.toLowerCase(), provider); } } + return providerMap; } private static getCustomProvider(cfg: IRemotesConfig) { diff --git a/src/gitCodeLensProvider.ts b/src/gitCodeLensProvider.ts index a7aa4a4..7f4d338 100644 --- a/src/gitCodeLensProvider.ts +++ b/src/gitCodeLensProvider.ts @@ -1,9 +1,9 @@ 'use strict'; import { Functions, Iterables } from './system'; -import { CancellationToken, CodeLens, CodeLensProvider, Command, commands, DocumentSelector, Event, EventEmitter, ExtensionContext, Position, Range, SymbolInformation, SymbolKind, TextDocument, Uri, workspace } from 'vscode'; +import { CancellationToken, CodeLens, CodeLensProvider, Command, commands, DocumentSelector, Event, EventEmitter, ExtensionContext, Position, Range, SymbolInformation, SymbolKind, TextDocument, Uri } from 'vscode'; import { Commands, DiffWithPreviousCommandArgs, ShowQuickCommitDetailsCommandArgs, ShowQuickCommitFileDetailsCommandArgs, ShowQuickFileHistoryCommandArgs } from './commands'; -import { BuiltInCommands, DocumentSchemes, ExtensionKey } from './constants'; -import { CodeLensCommand, CodeLensLocations, ICodeLensLanguageLocation, IConfig } from './configuration'; +import { CodeLensCommand, CodeLensLocations, configuration, ICodeLensConfig, ICodeLensLanguageLocation } from './configuration'; +import { BuiltInCommands, DocumentSchemes } from './constants'; import { GitBlame, GitBlameCommit, GitBlameLines, GitService, GitUri } from './gitService'; import { Logger } from './logger'; @@ -16,9 +16,10 @@ export class GitRecentChangeCodeLens extends CodeLens { public readonly blameRange: Range, public readonly isFullRange: boolean, range: Range, - public dirty: boolean + public readonly desiredCommand: CodeLensCommand, + command?: Command | undefined ) { - super(range); + super(range, command); } getBlame(): GitBlameLines | undefined { @@ -34,7 +35,8 @@ export class GitAuthorsCodeLens extends CodeLens { private readonly blame: () => GitBlameLines | undefined, public readonly blameRange: Range, public readonly isFullRange: boolean, - range: Range + range: Range, + public readonly desiredCommand: CodeLensCommand ) { super(range); } @@ -53,31 +55,29 @@ export class GitCodeLensProvider implements CodeLensProvider { static selector: DocumentSelector = { scheme: DocumentSchemes.File }; - private _config: IConfig; + private _debug: boolean; constructor( context: ExtensionContext, private readonly git: GitService - ) { - this._config = workspace.getConfiguration().get(ExtensionKey)!; - } + ) { } reset() { - this._config = workspace.getConfiguration().get(ExtensionKey)!; - - Logger.log('Triggering a reset of the git CodeLens provider'); this._onDidChangeCodeLenses.fire(); } async provideCodeLenses(document: TextDocument, token: CancellationToken): Promise { const dirty = document.isDirty; - let languageLocations = this._config.codeLens.perLanguageLocations.find(ll => ll.language !== undefined && ll.language.toLowerCase() === document.languageId); + const cfg = configuration.get(configuration.name('codeLens').value, document.uri); + this._debug = cfg.debug; + + let languageLocations = cfg.perLanguageLocations && cfg.perLanguageLocations.find(ll => ll.language !== undefined && ll.language.toLowerCase() === document.languageId); if (languageLocations == null) { languageLocations = { language: undefined, - locations: this._config.codeLens.locations, - customSymbols: this._config.codeLens.customLocationSymbols + locations: cfg.locations, + customSymbols: cfg.customLocationSymbols } as ICodeLensLanguageLocation; } @@ -118,9 +118,12 @@ export class GitCodeLensProvider implements CodeLensProvider { const documentRangeFn = Functions.once(() => document.validateRange(new Range(0, 1000000, 1000000, 1000000))); + // Since blame information isn't valid when there are unsaved changes -- update the lenses appropriately + const dirtyCommand = dirty ? { title: this.getDirtyTitle(cfg) } as Command : undefined; + if (symbols !== undefined) { Logger.log('GitCodeLensProvider.provideCodeLenses:', `${symbols.length} symbol(s) found`); - symbols.forEach(sym => this.provideCodeLens(lenses, document, dirty, sym, languageLocations!, documentRangeFn, blame, gitUri)); + symbols.forEach(sym => this.provideCodeLens(lenses, document, sym, languageLocations!, documentRangeFn, blame, gitUri, cfg, dirty, dirtyCommand)); } if ((languageLocations.locations.includes(CodeLensLocations.Document) || languageLocations.customSymbols.includes('file')) && !languageLocations.customSymbols.includes('!file')) { @@ -129,17 +132,34 @@ export class GitCodeLensProvider implements CodeLensProvider { const blameRange = documentRangeFn(); let blameForRangeFn: (() => GitBlameLines | undefined) | undefined = undefined; - if (dirty || this._config.codeLens.recentChange.enabled) { + if (dirty || cfg.recentChange.enabled) { if (!dirty) { blameForRangeFn = Functions.once(() => this.git.getBlameForRangeSync(blame!, gitUri!, blameRange)); } - lenses.push(new GitRecentChangeCodeLens(SymbolKind.File, gitUri, blameForRangeFn, blameRange, true, new Range(0, 0, 0, blameRange.start.character), dirty)); + lenses.push(new GitRecentChangeCodeLens( + SymbolKind.File, + gitUri, + blameForRangeFn, + blameRange, + true, + new Range(0, 0, 0, blameRange.start.character), + cfg.recentChange.command, + dirtyCommand + )); } - if (!dirty && this._config.codeLens.authors.enabled) { + if (!dirty && cfg.authors.enabled) { if (blameForRangeFn === undefined) { blameForRangeFn = Functions.once(() => this.git.getBlameForRangeSync(blame!, gitUri!, blameRange)); } - lenses.push(new GitAuthorsCodeLens(SymbolKind.File, gitUri, blameForRangeFn, blameRange, true, new Range(0, 1, 0, blameRange.start.character))); + lenses.push(new GitAuthorsCodeLens( + SymbolKind.File, + gitUri, + blameForRangeFn, + blameRange, + true, + new Range(0, 1, 0, blameRange.start.character), + cfg.authors.command + )); } } } @@ -206,7 +226,7 @@ export class GitCodeLensProvider implements CodeLensProvider { return valid ? range || symbol.location.range : undefined; } - private provideCodeLens(lenses: CodeLens[], document: TextDocument, dirty: boolean, symbol: SymbolInformation, languageLocation: ICodeLensLanguageLocation, documentRangeFn: () => Range, blame: GitBlame | undefined, gitUri: GitUri | undefined): void { + private provideCodeLens(lenses: CodeLens[], document: TextDocument, symbol: SymbolInformation, languageLocation: ICodeLensLanguageLocation, documentRangeFn: () => Range, blame: GitBlame | undefined, gitUri: GitUri | undefined, cfg: ICodeLensConfig, dirty: boolean, dirtyCommand: Command | undefined): void { const blameRange = this.validateSymbolAndGetBlameRange(symbol, languageLocation, documentRangeFn); if (blameRange === undefined) return; @@ -218,15 +238,15 @@ export class GitCodeLensProvider implements CodeLensProvider { let startChar = 0; let blameForRangeFn: (() => GitBlameLines | undefined) | undefined; - if (dirty || this._config.codeLens.recentChange.enabled) { + if (dirty || cfg.recentChange.enabled) { if (!dirty) { blameForRangeFn = Functions.once(() => this.git.getBlameForRangeSync(blame!, gitUri!, blameRange)); } - lenses.push(new GitRecentChangeCodeLens(symbol.kind, gitUri, blameForRangeFn, blameRange, false, line.range.with(new Position(line.range.start.line, startChar)), dirty)); + lenses.push(new GitRecentChangeCodeLens(symbol.kind, gitUri, blameForRangeFn, blameRange, false, line.range.with(new Position(line.range.start.line, startChar)), cfg.recentChange.command, dirtyCommand)); startChar++; } - if (this._config.codeLens.authors.enabled) { + if (cfg.authors.enabled) { let multiline = !blameRange.isSingleLine; // HACK for Omnisharp, since it doesn't return full ranges if (!multiline && document.languageId === 'csharp') { @@ -251,7 +271,7 @@ export class GitCodeLensProvider implements CodeLensProvider { if (blameForRangeFn === undefined) { blameForRangeFn = Functions.once(() => this.git.getBlameForRangeSync(blame!, gitUri!, blameRange)); } - lenses.push(new GitAuthorsCodeLens(symbol.kind, gitUri, blameForRangeFn, blameRange, false, line.range.with(new Position(line.range.start.line, startChar)))); + lenses.push(new GitAuthorsCodeLens(symbol.kind, gitUri, blameForRangeFn, blameRange, false, line.range.with(new Position(line.range.start.line, startChar)), cfg.authors.command)); } } } @@ -263,33 +283,16 @@ export class GitCodeLensProvider implements CodeLensProvider { } private resolveGitRecentChangeCodeLens(lens: GitRecentChangeCodeLens, token: CancellationToken): CodeLens { - // Since blame information isn't valid when there are unsaved changes -- update the lenses appropriately - let title: string; - if (lens.dirty) { - if (this._config.codeLens.recentChange.enabled && this._config.codeLens.authors.enabled) { - title = this._config.strings.codeLens.unsavedChanges.recentChangeAndAuthors; - } - else if (this._config.codeLens.recentChange.enabled) { - title = this._config.strings.codeLens.unsavedChanges.recentChangeOnly; - } - else { - title = this._config.strings.codeLens.unsavedChanges.authorsOnly; - } - - lens.command = { title: title } as Command; - return lens; - } - const blame = lens.getBlame(); if (blame === undefined) return lens; const recentCommit = Iterables.first(blame.commits.values()); - title = `${recentCommit.author}, ${recentCommit.fromNow()}`; - if (this._config.codeLens.debug) { + let title = `${recentCommit.author}, ${recentCommit.fromNow()}`; + if (this._debug) { title += ` [${SymbolKind[lens.symbolKind]}(${lens.range.start.character}-${lens.range.end.character}), Lines (${lens.blameRange.start.line + 1}-${lens.blameRange.end.line + 1}), Commit (${recentCommit.shortSha})]`; } - switch (this._config.codeLens.recentChange.command) { + switch (lens.desiredCommand) { case CodeLensCommand.DiffWithPrevious: return this.applyDiffWithPreviousCommand(title, lens, blame, recentCommit); case CodeLensCommand.ShowQuickCommitDetails: return this.applyShowQuickCommitDetailsCommand(title, lens, blame, recentCommit); case CodeLensCommand.ShowQuickCommitFileDetails: return this.applyShowQuickCommitFileDetailsCommand(title, lens, blame, recentCommit); @@ -306,11 +309,11 @@ export class GitCodeLensProvider implements CodeLensProvider { const count = blame.authors.size; let title = `${count} ${count > 1 ? 'authors' : 'author'} (${Iterables.first(blame.authors.values()).name}${count > 1 ? ' and others' : ''})`; - if (this._config.codeLens.debug) { + if (this._debug) { title += ` [${SymbolKind[lens.symbolKind]}(${lens.range.start.character}-${lens.range.end.character}), Lines (${lens.blameRange.start.line + 1}-${lens.blameRange.end.line + 1}), Authors (${Iterables.join(Iterables.map(blame.authors.values(), a => a.name), ', ')})]`; } - switch (this._config.codeLens.authors.command) { + switch (lens.desiredCommand) { case CodeLensCommand.DiffWithPrevious: return this.applyDiffWithPreviousCommand(title, lens, blame); case CodeLensCommand.ShowQuickCommitDetails: return this.applyShowQuickCommitDetailsCommand(title, lens, blame); case CodeLensCommand.ShowQuickCommitFileDetails: return this.applyShowQuickCommitFileDetailsCommand(title, lens, blame); @@ -400,4 +403,16 @@ export class GitCodeLensProvider implements CodeLensProvider { }; return lens; } + + private getDirtyTitle(cfg: ICodeLensConfig) { + if (cfg.recentChange.enabled && cfg.authors.enabled) { + return configuration.get(configuration.name('strings')('codeLens')('unsavedChanges')('recentChangeAndAuthors').value); + } + else if (cfg.recentChange.enabled) { + return configuration.get(configuration.name('strings')('codeLens')('unsavedChanges')('recentChangeOnly').value); + } + else { + return configuration.get(configuration.name('strings')('codeLens')('unsavedChanges')('authorsOnly').value); + } + } } \ No newline at end of file diff --git a/src/gitRevisionCodeLensProvider.ts b/src/gitRevisionCodeLensProvider.ts index 51507ca..18da153 100644 --- a/src/gitRevisionCodeLensProvider.ts +++ b/src/gitRevisionCodeLensProvider.ts @@ -1,5 +1,4 @@ 'use strict'; -// import { Iterables } from './system'; import { CancellationToken, CodeLens, CodeLensProvider, DocumentSelector, ExtensionContext, Range, TextDocument, Uri } from 'vscode'; import { Commands, DiffWithPreviousCommandArgs, DiffWithWorkingCommandArgs } from './commands'; import { DocumentSchemes } from './constants'; @@ -8,9 +7,8 @@ import { GitCommit, GitService, GitUri } from './gitService'; export class GitDiffWithWorkingCodeLens extends CodeLens { constructor( - git: GitService, - public fileName: string, - public commit: GitCommit, + public readonly fileName: string, + public readonly commit: GitCommit, range: Range ) { super(range); @@ -20,9 +18,8 @@ export class GitDiffWithWorkingCodeLens extends CodeLens { export class GitDiffWithPreviousCodeLens extends CodeLens { constructor( - git: GitService, - public fileName: string, - public commit: GitCommit, + public readonly fileName: string, + public readonly commit: GitCommit, range: Range ) { super(range); @@ -33,7 +30,10 @@ export class GitRevisionCodeLensProvider implements CodeLensProvider { static selector: DocumentSelector = { scheme: DocumentSchemes.GitLensGit }; - constructor(context: ExtensionContext, private git: GitService) { } + constructor( + context: ExtensionContext, + private readonly git: GitService + ) { } async provideCodeLenses(document: TextDocument, token: CancellationToken): Promise { const data = GitService.fromGitContentUri(document.uri); @@ -45,9 +45,9 @@ export class GitRevisionCodeLensProvider implements CodeLensProvider { if (commit === undefined) return lenses; if (commit.previousSha) { - lenses.push(new GitDiffWithPreviousCodeLens(this.git, commit.previousUri.fsPath, commit, new Range(0, 0, 0, 1))); + lenses.push(new GitDiffWithPreviousCodeLens(commit.previousUri.fsPath, commit, new Range(0, 0, 0, 1))); } - lenses.push(new GitDiffWithWorkingCodeLens(this.git, commit.uri.fsPath, commit, new Range(0, 1, 0, 2))); + lenses.push(new GitDiffWithWorkingCodeLens(commit.uri.fsPath, commit, new Range(0, 1, 0, 2))); return lenses; } diff --git a/src/gitService.ts b/src/gitService.ts index 5a444ae..70a157b 100644 --- a/src/gitService.ts +++ b/src/gitService.ts @@ -1,10 +1,10 @@ 'use strict'; -import { Functions, Iterables, Objects } from './system'; -import { Disposable, Event, EventEmitter, Range, TextDocument, TextDocumentChangeEvent, TextEditor, Uri, window, WindowState, workspace, WorkspaceFoldersChangeEvent } from 'vscode'; -import { IConfig } from './configuration'; -import { CommandContext, DocumentSchemes, ExtensionKey, setCommandContext } from './constants'; -import { RemoteProviderFactory } from './git/remotes/factory'; -import { Git, GitAuthor, GitBlame, GitBlameCommit, GitBlameLine, GitBlameLines, GitBlameParser, GitBranch, GitBranchParser, GitCommit, GitCommitType, GitDiff, GitDiffChunkLine, GitDiffParser, GitDiffShortStat, GitLog, GitLogCommit, GitLogParser, GitRemote, GitRemoteParser, GitStash, GitStashParser, GitStatus, GitStatusFile, GitStatusParser, IGit, Repository, setDefaultEncoding } from './git/git'; +import { Functions, Iterables } from './system'; +import { ConfigurationChangeEvent, Disposable, Event, EventEmitter, Range, TextDocument, TextDocumentChangeEvent, TextEditor, Uri, window, WindowState, workspace, WorkspaceFoldersChangeEvent } from 'vscode'; +import { configuration, IConfig, IRemotesConfig } from './configuration'; +import { CommandContext, DocumentSchemes, setCommandContext } from './constants'; +import { RemoteProviderFactory, RemoteProviderMap } from './git/remotes/factory'; +import { Git, GitAuthor, GitBlame, GitBlameCommit, GitBlameLine, GitBlameLines, GitBlameParser, GitBranch, GitBranchParser, GitCommit, GitCommitType, GitDiff, GitDiffChunkLine, GitDiffParser, GitDiffShortStat, GitLog, GitLogCommit, GitLogParser, GitRemote, GitRemoteParser, GitStash, GitStashParser, GitStatus, GitStatusFile, GitStatusParser, IGit, Repository } from './git/git'; import { GitUri, IGitCommitInfo, IGitUriData } from './git/gitUri'; import { Logger } from './logger'; import * as fs from 'fs'; @@ -71,7 +71,6 @@ export enum GitRepoSearchBy { export enum GitChangeReason { GitCache = 'git-cache', - RemoteCache = 'remote-cache', Repositories = 'repositories' } @@ -101,7 +100,6 @@ export class GitService extends Disposable { private _disposable: Disposable | undefined; private _documentKeyMap: Map; private _gitCache: Map; - private _remotesCache: Map; private _repositories: Map; private _repositoriesPromise: Promise | undefined; private _suspended: boolean = false; @@ -113,20 +111,17 @@ export class GitService extends Disposable { this._documentKeyMap = new Map(); this._gitCache = new Map(); - this._remotesCache = new Map(); this._repositories = new Map(); this._trackedCache = new Map(); this._versionedUriCache = new Map(); - this.onConfigurationChanged(); - this._repositoriesPromise = this.onWorkspaceFoldersChanged(); - this._disposable = Disposable.from( window.onDidChangeWindowState(this.onWindowStateChanged, this), - workspace.onDidChangeConfiguration(this.onConfigurationChanged, this), workspace.onDidChangeWorkspaceFolders(this.onWorkspaceFoldersChanged, this), - RemoteProviderFactory.onDidChange(this.onRemoteProvidersChanged, this) + configuration.onDidChange(this.onConfigurationChanged, this) ); + this.onConfigurationChanged(configuration.initializingChangeEvent); + this._repositoriesPromise = this.onWorkspaceFoldersChanged(); } dispose() { @@ -139,7 +134,6 @@ export class GitService extends Disposable { this._documentKeyMap.clear(); this._gitCache.clear(); - this._remotesCache.clear(); this._trackedCache.clear(); this._versionedUriCache.clear(); } @@ -160,13 +154,12 @@ export class GitService extends Disposable { this._trackedCache.clear(); } - private onConfigurationChanged() { - const encoding = workspace.getConfiguration('files').get('encoding', 'utf8'); - setDefaultEncoding(encoding); + private onConfigurationChanged(e: ConfigurationChangeEvent) { + const initializing = configuration.initializing(e); - const cfg = workspace.getConfiguration().get(ExtensionKey)!; + const cfg = configuration.get(); - if (!Objects.areEquivalent(cfg.advanced, this.config && this.config.advanced)) { + if (initializing || configuration.changed(e, configuration.name('advanced')('caching')('enabled').value)) { if (cfg.advanced.caching.enabled) { this._cacheDisposable && this._cacheDisposable.dispose(); @@ -184,26 +177,15 @@ export class GitService extends Disposable { } } - // Only count the change if we aren't just starting up - const ignoreWhitespace = this.config === undefined - ? cfg.blame.ignoreWhitespace - : this.config.blame.ignoreWhitespace; - this.config = cfg; - if (this.config.blame.ignoreWhitespace !== ignoreWhitespace) { + // Only count the change if we aren't initializing + if (!initializing && configuration.changed(e, configuration.name('blame')('ignoreWhitespace').value, null)) { this._gitCache.clear(); - this.fireChange(GitChangeReason.GitCache); } } - private onRemoteProvidersChanged() { - this._remotesCache.clear(); - - this.fireChange(GitChangeReason.RemoteCache); - } - private onTextDocumentChanged(e: TextDocumentChangeEvent) { let key = this._documentKeyMap.get(e.document); if (key === undefined) { @@ -260,8 +242,13 @@ export class GitService extends Disposable { this._repositories.set(fsPath, undefined); } else { - this._repositories.set(fsPath, new Repository(f, rp, this.onAnyRepositoryChanged.bind(this), this._suspended)); + this._repositories.set(fsPath, new Repository(f, rp, this, this.onAnyRepositoryChanged.bind(this), this._suspended)); } + + // const repoPaths = await this.searchForRepositories(f); + // if (repoPaths.length !== 0) { + // debugger; + // } } for (const f of e.removed) { @@ -283,6 +270,11 @@ export class GitService extends Disposable { } } + // private async searchForRepositories(folder: WorkspaceFolder): Promise { + // const uris = await workspace.findFiles(new RelativePattern(folder, '**/.git/HEAD')); + // return Arrays.filterMapAsync(uris, uri => this.getRepoPathCore(path.dirname(uri.fsPath), true)); + // } + private fireChange(reason: GitChangeReason) { this._onDidChange.fire({ reason: reason }); } @@ -631,7 +623,7 @@ export class GitService extends Disposable { Logger.log(`getDiffForFile('${uri.repoPath}', '${uri.fsPath}', '${sha1}', '${sha2}')`); } - const promise = this.getDiffForFileCore(uri.repoPath, uri.fsPath, sha1, sha2, entry, key); + const promise = this.getDiffForFileCore(uri.repoPath, uri.fsPath, sha1, sha2, { encoding: GitService.getEncoding(uri) }, entry, key); if (entry) { Logger.log(`Add log cache for '${entry.key}:${key}'`); @@ -644,11 +636,11 @@ export class GitService extends Disposable { return promise; } - private async getDiffForFileCore(repoPath: string | undefined, fileName: string, sha1: string | undefined, sha2: string | undefined, entry: GitCacheEntry | undefined, key: string): Promise { + private async getDiffForFileCore(repoPath: string | undefined, fileName: string, sha1: string | undefined, sha2: string | undefined, options: { encoding?: string }, entry: GitCacheEntry | undefined, key: string): Promise { const [file, root] = Git.splitPath(fileName, repoPath, false); try { - const data = await Git.diff(root, file, sha1, sha2); + const data = await Git.diff(root, file, sha1, sha2, options); const diff = GitDiffParser.parse(data); return diff; } @@ -879,35 +871,35 @@ export class GitService extends Disposable { } } - hasRemotes(repoPath: string | undefined): boolean { + async hasRemotes(repoPath: string | undefined): Promise { if (repoPath === undefined) return false; - const remotes = this._remotesCache.get(this.normalizeRepoPath(repoPath)); - return remotes !== undefined && remotes.length > 0; - } + const repository = await this.getRepository(repoPath); + if (repository === undefined) return false; - private normalizeRepoPath(repoPath: string) { - return (repoPath.endsWith('/') ? repoPath : `${repoPath}/`).toLowerCase(); + return repository.hasRemotes(); } async getRemotes(repoPath: string | undefined): Promise { - if (!repoPath) return []; + if (repoPath === undefined) return []; Logger.log(`getRemotes('${repoPath}')`); - const normalizedRepoPath = this.normalizeRepoPath(repoPath); + const repository = await this.getRepository(repoPath); + if (repository !== undefined) return repository.getRemotes(); + + return this.getRemotesCore(repoPath); + } - let remotes = this._remotesCache.get(normalizedRepoPath); - if (remotes !== undefined) return remotes; + async getRemotesCore(repoPath: string | undefined, providerMap?: RemoteProviderMap): Promise { + if (repoPath === undefined) return []; - const data = await Git.remote(repoPath); - remotes = GitRemoteParser.parse(data, repoPath); + Logger.log(`getRemotesCore('${repoPath}')`); - if (remotes !== undefined) { - this._remotesCache.set(normalizedRepoPath, remotes); - } + providerMap = providerMap || RemoteProviderFactory.createMap(configuration.get(configuration.name('remotes').value, null)); - return remotes; + const data = await Git.remote(repoPath); + return GitRemoteParser.parse(data, repoPath, RemoteProviderFactory.factory(providerMap)); } async getRepoPath(filePath: string): Promise; @@ -964,9 +956,10 @@ export class GitService extends Disposable { } async getStashList(repoPath: string | undefined): Promise { - Logger.log(`getStashList('${repoPath}')`); if (repoPath === undefined) return undefined; + Logger.log(`getStashList('${repoPath}')`); + const data = await Git.stash_list(repoPath); const stash = GitStashParser.parse(data, repoPath); return stash; @@ -985,9 +978,10 @@ export class GitService extends Disposable { } async getStatusForRepo(repoPath: string | undefined): Promise { - Logger.log(`getStatusForRepo('${repoPath}')`); if (repoPath === undefined) return undefined; + Logger.log(`getStatusForRepo('${repoPath}')`); + const porcelainVersion = Git.validateVersion(2, 11) ? 2 : 1; const data = await Git.status(repoPath, porcelainVersion); @@ -1010,7 +1004,7 @@ export class GitService extends Disposable { getVersionedFileText(repoPath: string, fileName: string, sha: string) { Logger.log(`getVersionedFileText('${repoPath}', '${fileName}', ${sha})`); - return Git.show(repoPath, fileName, sha); + return Git.show(repoPath, fileName, sha, { encoding: GitService.getEncoding(repoPath, fileName) }); } hasGitUriForFile(editor: TextEditor): boolean { @@ -1126,6 +1120,15 @@ export class GitService extends Disposable { return Git.stash_push(repoPath, pathspecs, message); } + static getEncoding(repoPath: string, fileName: string): string; + static getEncoding(uri: Uri): string; + static getEncoding(repoPathOrUri: string | Uri, fileName?: string): string { + const uri = (typeof repoPathOrUri === 'string') + ? Uri.file(path.join(repoPathOrUri, fileName!)) + : repoPathOrUri; + return Git.getEncoding(workspace.getConfiguration('files', uri).get('encoding')); + } + static getGitPath(gitPath?: string): Promise { return Git.getGitPath(gitPath); } @@ -1191,14 +1194,14 @@ export class GitService extends Disposable { } const parsed = path.parse(fileName!); - return Uri.parse(`${DocumentSchemes.GitLensGit}:${parsed.dir}${parsed.name}:${shortSha}${parsed.ext}?${JSON.stringify(data)}`); + return Uri.parse(`${DocumentSchemes.GitLensGit}:${path.join(parsed.dir, parsed.name)}:${shortSha}${parsed.ext}?${JSON.stringify(data)}`); } private static toGitUriData(commit: IGitUriData, originalFileName?: string): T { - const fileName = Git.normalizePath(path.resolve(commit.repoPath, commit.fileName)); + const fileName = Git.normalizePath(path.relative(commit.repoPath, commit.fileName)); const data = { repoPath: commit.repoPath, fileName: fileName, sha: commit.sha } as T; if (originalFileName) { - data.originalFileName = Git.normalizePath(path.resolve(commit.repoPath, originalFileName)); + data.originalFileName = Git.normalizePath(path.relative(commit.repoPath, originalFileName)); } return data; } diff --git a/src/logger.ts b/src/logger.ts index 8fc9467..3b1a36c 100644 --- a/src/logger.ts +++ b/src/logger.ts @@ -1,7 +1,7 @@ 'use strict'; -import { ExtensionContext, OutputChannel, window, workspace } from 'vscode'; -import { IConfig } from './configuration'; -import { ExtensionKey, ExtensionOutputChannelName } from './constants'; +import { ConfigurationChangeEvent, ExtensionContext, OutputChannel, window } from 'vscode'; +import { configuration } from './configuration'; +import { ExtensionOutputChannelName } from './constants'; import { Telemetry } from './telemetry'; const ConsolePrefix = `[${ExtensionOutputChannelName}]`; @@ -12,63 +12,70 @@ export enum OutputLevel { Verbose = 'verbose' } -let debug = false; -let level: OutputLevel = OutputLevel.Silent; -let output: OutputChannel; - -function onConfigurationChanged() { - const cfg = workspace.getConfiguration().get(ExtensionKey); - if (cfg === undefined) return; +export class Logger { - if (cfg.debug !== debug || cfg.outputLevel !== level) { - debug = cfg.debug; - level = cfg.outputLevel; + static debug = false; + static level: OutputLevel = OutputLevel.Silent; + static output: OutputChannel | undefined; - if (level === OutputLevel.Silent) { - output && output.dispose(); - } - else { - output = output || window.createOutputChannel(ExtensionOutputChannelName); - } + static configure(context: ExtensionContext) { + context.subscriptions.push(configuration.onDidChange(this.onConfigurationChanged, this)); + this.onConfigurationChanged(configuration.initializingChangeEvent); } -} -export class Logger { + private static onConfigurationChanged(e: ConfigurationChangeEvent) { + const initializing = configuration.initializing(e); - static configure(context: ExtensionContext) { - context.subscriptions.push(workspace.onDidChangeConfiguration(onConfigurationChanged)); - onConfigurationChanged(); + let section = configuration.name('debug').value; + if (initializing || configuration.changed(e, section)) { + this.debug = configuration.get(section); + } + + section = configuration.name('outputLevel').value; + if (initializing || configuration.changed(e, section)) { + this.level = configuration.get(section); + + if (this.level === OutputLevel.Silent) { + if (this.output !== undefined) { + this.output.dispose(); + this.output = undefined; + } + } + else { + this.output = this.output || window.createOutputChannel(ExtensionOutputChannelName); + } + } } static log(message?: any, ...params: any[]): void { - if (debug) { + if (this.debug) { console.log(this.timestamp, ConsolePrefix, message, ...params); } - if (level === OutputLevel.Verbose) { - output.appendLine((debug ? [this.timestamp, message, ...params] : [message, ...params]).join(' ')); + if (this.output !== undefined && this.level === OutputLevel.Verbose) { + this.output.appendLine((this.debug ? [this.timestamp, message, ...params] : [message, ...params]).join(' ')); } } static error(ex: Error, classOrMethod?: string, ...params: any[]): void { - if (debug) { + if (this.debug) { console.error(this.timestamp, ConsolePrefix, classOrMethod, ex, ...params); } - if (level !== OutputLevel.Silent) { - output.appendLine((debug ? [this.timestamp, classOrMethod, ex, ...params] : [classOrMethod, ex, ...params]).join(' ')); + if (this.output !== undefined && this.level !== OutputLevel.Silent) { + this.output.appendLine((this.debug ? [this.timestamp, classOrMethod, ex, ...params] : [classOrMethod, ex, ...params]).join(' ')); } Telemetry.trackException(ex); } static warn(message?: any, ...params: any[]): void { - if (debug) { + if (this.debug) { console.warn(this.timestamp, ConsolePrefix, message, ...params); } - if (level !== OutputLevel.Silent) { - output.appendLine((debug ? [this.timestamp, message, ...params] : [message, ...params]).join(' ')); + if (this.output !== undefined && this.level !== OutputLevel.Silent) { + this.output.appendLine((this.debug ? [this.timestamp, message, ...params] : [message, ...params]).join(' ')); } } diff --git a/src/system/function.ts b/src/system/function.ts index 9ab6d82..fd16d2d 100644 --- a/src/system/function.ts +++ b/src/system/function.ts @@ -7,11 +7,28 @@ export interface IDeferred { flush(...args: any[]): void; } +interface IPropOfValue { + (): any; + value: string | undefined; +} + export namespace Functions { export function debounce(fn: T, wait?: number, options?: { leading?: boolean, maxWait?: number, trailing?: boolean }): T & IDeferred { return _debounce(fn, wait, options); } + export function propOf(o: T, key: K) { + const propOfCore = (o: T, key: K) => { + const value: string = (propOfCore as IPropOfValue).value === undefined + ? key + : `${(propOfCore as IPropOfValue).value}.${key}`; + (propOfCore as IPropOfValue).value = value; + const fn = (k: Y) => propOfCore(o[key], k); + return Object.assign(fn, { value: value }); + }; + return propOfCore(o, key); + } + export function once(fn: T): T { return _once(fn); } diff --git a/src/views/branchHistoryNode.ts b/src/views/branchHistoryNode.ts index 27ee27a..822f735 100644 --- a/src/views/branchHistoryNode.ts +++ b/src/views/branchHistoryNode.ts @@ -1,10 +1,11 @@ 'use strict'; import { Iterables } from '../system'; -import { ExtensionContext, TreeItem, TreeItemCollapsibleState } from 'vscode'; +import { TreeItem, TreeItemCollapsibleState } from 'vscode'; import { CommitNode } from './commitNode'; import { GlyphChars } from '../constants'; import { ExplorerNode, ResourceType, ShowAllNode } from './explorerNode'; -import { GitBranch, GitService, GitUri } from '../gitService'; +import { GitExplorer } from './gitExplorer'; +import { GitBranch, GitUri } from '../gitService'; export class BranchHistoryNode extends ExplorerNode { @@ -15,34 +16,33 @@ export class BranchHistoryNode extends ExplorerNode { constructor( public readonly branch: GitBranch, uri: GitUri, - protected readonly context: ExtensionContext, - protected readonly git: GitService + private readonly explorer: GitExplorer ) { super(uri); } async getChildren(): Promise { - const log = await this.git.getLogForRepo(this.uri.repoPath!, this.branch.name, this.maxCount); + const log = await this.explorer.git.getLogForRepo(this.uri.repoPath!, this.branch.name, this.maxCount); if (log === undefined) return []; - const children: (CommitNode | ShowAllNode)[] = [...Iterables.map(log.commits.values(), c => new CommitNode(c, this.context, this.git, this.branch))]; + const children: (CommitNode | ShowAllNode)[] = [...Iterables.map(log.commits.values(), c => new CommitNode(c, this.explorer, this.branch))]; if (log.truncated) { - children.push(new ShowAllNode('Show All Commits', this, this.context)); + children.push(new ShowAllNode('Show All Commits', this, this.explorer.context)); } return children; } async getTreeItem(): Promise { let name = this.branch.getName(); - if (!this.branch.remote && this.branch.tracking !== undefined && this.git.config.gitExplorer.showTrackingBranch) { + if (!this.branch.remote && this.branch.tracking !== undefined && this.explorer.config.showTrackingBranch) { name += ` ${GlyphChars.Space}${GlyphChars.ArrowLeftRight}${GlyphChars.Space} ${this.branch.tracking}`; } const item = new TreeItem(`${this.branch!.current ? `${GlyphChars.Check} ${GlyphChars.Space}` : ''}${name}`, TreeItemCollapsibleState.Collapsed); item.contextValue = this.branch.tracking ? `${this.resourceType}:remote` : this.resourceType; item.iconPath = { - dark: this.context.asAbsolutePath('images/dark/icon-branch.svg'), - light: this.context.asAbsolutePath('images/light/icon-branch.svg') + dark: this.explorer.context.asAbsolutePath('images/dark/icon-branch.svg'), + light: this.explorer.context.asAbsolutePath('images/light/icon-branch.svg') }; return item; diff --git a/src/views/branchesNode.ts b/src/views/branchesNode.ts index 5b95393..12f53e7 100644 --- a/src/views/branchesNode.ts +++ b/src/views/branchesNode.ts @@ -1,9 +1,10 @@ 'use strict'; import { Iterables } from '../system'; -import { ExtensionContext, TreeItem, TreeItemCollapsibleState } from 'vscode'; +import { TreeItem, TreeItemCollapsibleState } from 'vscode'; import { BranchHistoryNode } from './branchHistoryNode'; import { ExplorerNode, ResourceType } from './explorerNode'; -import { GitService, GitUri } from '../gitService'; +import { GitExplorer } from './gitExplorer'; +import { GitUri, Repository } from '../gitService'; export class BranchesNode extends ExplorerNode { @@ -11,31 +12,31 @@ export class BranchesNode extends ExplorerNode { constructor( uri: GitUri, - protected readonly context: ExtensionContext, - protected readonly git: GitService + private readonly repo: Repository, + private readonly explorer: GitExplorer ) { super(uri); } async getChildren(): Promise { - const branches = await this.git.getBranches(this.uri.repoPath!); + 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)); - return [...Iterables.filterMap(branches, b => b.remote ? undefined : new BranchHistoryNode(b, this.uri, this.context, this.git))]; + return [...Iterables.filterMap(branches, b => b.remote ? undefined : new BranchHistoryNode(b, this.uri, this.explorer))]; } async getTreeItem(): Promise { const item = new TreeItem(`Branches`, TreeItemCollapsibleState.Expanded); - const remotes = await this.git.getRemotes(this.uri.repoPath!); + const remotes = await this.repo.getRemotes(); item.contextValue = (remotes !== undefined && remotes.length > 0) ? `${this.resourceType}:remote` : this.resourceType; item.iconPath = { - dark: this.context.asAbsolutePath('images/dark/icon-branch.svg'), - light: this.context.asAbsolutePath('images/light/icon-branch.svg') + dark: this.explorer.context.asAbsolutePath('images/dark/icon-branch.svg'), + light: this.explorer.context.asAbsolutePath('images/light/icon-branch.svg') }; return item; diff --git a/src/views/commitFileNode.ts b/src/views/commitFileNode.ts index 3e9d226..b8bc212 100644 --- a/src/views/commitFileNode.ts +++ b/src/views/commitFileNode.ts @@ -1,8 +1,9 @@ 'use strict'; -import { Command, ExtensionContext, TreeItem, TreeItemCollapsibleState, Uri } from 'vscode'; +import { Command, TreeItem, TreeItemCollapsibleState, Uri } from 'vscode'; import { Commands, DiffWithPreviousCommandArgs } from '../commands'; import { ExplorerNode, ResourceType } from './explorerNode'; -import { CommitFormatter, getGitStatusIcon, GitBranch, GitCommit, GitCommitType, GitService, GitUri, ICommitFormatOptions, IGitStatusFile, IStatusFormatOptions, StatusFileFormatter } from '../gitService'; +import { GitExplorer } from './gitExplorer'; +import { CommitFormatter, getGitStatusIcon, GitBranch, GitCommit, GitCommitType, GitUri, ICommitFormatOptions, IGitStatusFile, IStatusFormatOptions, StatusFileFormatter } from '../gitService'; import * as path from 'path'; export enum CommitFileNodeDisplayAs { @@ -24,8 +25,7 @@ export class CommitFileNode extends ExplorerNode { constructor( public readonly status: IGitStatusFile, public commit: GitCommit, - protected readonly context: ExtensionContext, - protected readonly git: GitService, + protected readonly explorer: GitExplorer, private displayAs: CommitFileNodeDisplayAs = CommitFileNodeDisplayAs.Commit, public readonly branch?: GitBranch ) { @@ -39,7 +39,7 @@ export class CommitFileNode extends ExplorerNode { async getTreeItem(): Promise { if (this.commit.type !== GitCommitType.File) { - const log = await this.git.getLogForFile(this.repoPath, this.status.fileName, this.commit.sha, { maxCount: 2 }); + const log = await this.explorer.git.getLogForFile(this.repoPath, this.status.fileName, this.commit.sha, { maxCount: 2 }); if (log !== undefined) { this.commit = log.commits.get(this.commit.sha) || this.commit; } @@ -53,8 +53,8 @@ export class CommitFileNode extends ExplorerNode { : getGitStatusIcon(this.status.status); item.iconPath = { - dark: this.context.asAbsolutePath(path.join('images', 'dark', icon)), - light: this.context.asAbsolutePath(path.join('images', 'light', icon)) + dark: this.explorer.context.asAbsolutePath(path.join('images', 'dark', icon)), + light: this.explorer.context.asAbsolutePath(path.join('images', 'light', icon)) }; item.command = this.getCommand(); @@ -79,7 +79,7 @@ export class CommitFileNode extends ExplorerNode { this._label = (this.displayAs & CommitFileNodeDisplayAs.CommitLabel) ? CommitFormatter.fromTemplate(this.getCommitTemplate(), this.commit, { truncateMessageAtNewLine: true, - dataFormat: this.git.config.defaultDateFormat + dataFormat: this.explorer.git.config.defaultDateFormat } as ICommitFormatOptions) : StatusFileFormatter.fromTemplate(this.getCommitFileTemplate(), this.status, @@ -98,11 +98,11 @@ export class CommitFileNode extends ExplorerNode { } protected getCommitTemplate() { - return this.git.config.gitExplorer.commitFormat; + return this.explorer.config.commitFormat; } protected getCommitFileTemplate() { - return this.git.config.gitExplorer.commitFileFormat; + return this.explorer.config.commitFileFormat; } getCommand(): Command | undefined { diff --git a/src/views/commitNode.ts b/src/views/commitNode.ts index 38c0aa9..8883105 100644 --- a/src/views/commitNode.ts +++ b/src/views/commitNode.ts @@ -1,11 +1,12 @@ 'use strict'; import { Arrays, Iterables } from '../system'; -import { Command, ExtensionContext, TreeItem, TreeItemCollapsibleState } from 'vscode'; +import { Command, TreeItem, TreeItemCollapsibleState } from 'vscode'; import { Commands, DiffWithPreviousCommandArgs } from '../commands'; import { CommitFileNode, CommitFileNodeDisplayAs } from './commitFileNode'; import { GitExplorerFilesLayout } from '../configuration'; import { FolderNode, IFileExplorerNode } from './folderNode'; import { ExplorerNode, ResourceType } from './explorerNode'; +import { GitExplorer } from './gitExplorer'; import { CommitFormatter, GitBranch, GitLogCommit, GitService, GitUri, ICommitFormatOptions } from '../gitService'; import * as path from 'path'; @@ -16,8 +17,7 @@ export class CommitNode extends ExplorerNode { constructor( public readonly commit: GitLogCommit, - protected readonly context: ExtensionContext, - protected readonly git: GitService, + private readonly explorer: GitExplorer, public readonly branch?: GitBranch ) { super(new GitUri(commit.uri, commit)); @@ -27,21 +27,21 @@ export class CommitNode extends ExplorerNode { async getChildren(): Promise { const repoPath = this.repoPath; - const log = await this.git.getLogForRepo(repoPath, this.commit.sha, 1); + const log = await this.explorer.git.getLogForRepo(repoPath, this.commit.sha, 1); if (log === undefined) return []; const commit = Iterables.first(log.commits.values()); if (commit === undefined) return []; let children: IFileExplorerNode[] = [ - ...Iterables.map(commit.fileStatuses, s => new CommitFileNode(s, commit, this.context, this.git, CommitFileNodeDisplayAs.File, this.branch)) + ...Iterables.map(commit.fileStatuses, s => new CommitFileNode(s, commit, this.explorer, CommitFileNodeDisplayAs.File, this.branch)) ]; - if (this.git.config.gitExplorer.files.layout !== GitExplorerFilesLayout.List) { + if (this.explorer.config.files.layout !== GitExplorerFilesLayout.List) { const hierarchy = Arrays.makeHierarchical(children, n => n.uri.getRelativePath().split('/'), - (...paths: string[]) => GitService.normalizePath(path.join(...paths)), this.git.config.gitExplorer.files.compact); + (...paths: string[]) => GitService.normalizePath(path.join(...paths)), this.explorer.config.files.compact); - const root = new FolderNode(repoPath, '', undefined, hierarchy, this.git.config.gitExplorer); + const root = new FolderNode(repoPath, '', undefined, hierarchy, this.explorer); children = await root.getChildren() as IFileExplorerNode[]; } else { @@ -51,15 +51,15 @@ export class CommitNode extends ExplorerNode { } getTreeItem(): TreeItem { - const item = new TreeItem(CommitFormatter.fromTemplate(this.git.config.gitExplorer.commitFormat, this.commit, { + const item = new TreeItem(CommitFormatter.fromTemplate(this.explorer.config.commitFormat, this.commit, { truncateMessageAtNewLine: true, - dataFormat: this.git.config.defaultDateFormat + dataFormat: this.explorer.git.config.defaultDateFormat } as ICommitFormatOptions), TreeItemCollapsibleState.Collapsed); item.contextValue = this.resourceType; item.iconPath = { - dark: this.context.asAbsolutePath('images/dark/icon-commit.svg'), - light: this.context.asAbsolutePath('images/light/icon-commit.svg') + dark: this.explorer.context.asAbsolutePath('images/dark/icon-commit.svg'), + light: this.explorer.context.asAbsolutePath('images/light/icon-commit.svg') }; return item; diff --git a/src/views/fileHistoryNode.ts b/src/views/fileHistoryNode.ts index 8568eb9..34c21a6 100644 --- a/src/views/fileHistoryNode.ts +++ b/src/views/fileHistoryNode.ts @@ -23,7 +23,7 @@ export class FileHistoryNode extends ExplorerNode { const log = await this.explorer.git.getLogForFile(this.uri.repoPath, this.uri.fsPath, this.uri.sha); if (log === undefined) return [new MessageNode('No file history')]; - return [...Iterables.map(log.commits.values(), c => new CommitFileNode(c.fileStatuses[0], c, this.explorer.context, this.explorer.git, CommitFileNodeDisplayAs.CommitLabel | CommitFileNodeDisplayAs.StatusIcon))]; + return [...Iterables.map(log.commits.values(), c => new CommitFileNode(c.fileStatuses[0], c, this.explorer, CommitFileNodeDisplayAs.CommitLabel | CommitFileNodeDisplayAs.StatusIcon))]; } getTreeItem(): TreeItem { diff --git a/src/views/folderNode.ts b/src/views/folderNode.ts index 8a88e08..1db3e06 100644 --- a/src/views/folderNode.ts +++ b/src/views/folderNode.ts @@ -3,6 +3,7 @@ import { Arrays, Objects } from '../system'; import { TreeItem, TreeItemCollapsibleState, Uri } from 'vscode'; import { GitExplorerFilesLayout, IGitExplorerConfig } from '../configuration'; import { ExplorerNode, ResourceType } from './explorerNode'; +import { GitExplorer } from './gitExplorer'; import { GitUri } from '../gitService'; export interface IFileExplorerNode extends ExplorerNode { @@ -23,7 +24,7 @@ export class FolderNode extends ExplorerNode { public readonly folderName: string, public readonly relativePath: string | undefined, public readonly root: Arrays.IHierarchicalItem, - private readonly config: IGitExplorerConfig + private readonly explorer: GitExplorer ) { super(new GitUri(Uri.file(repoPath), { repoPath: repoPath, fileName: repoPath })); } @@ -33,12 +34,12 @@ export class FolderNode extends ExplorerNode { let children: (FolderNode | IFileExplorerNode)[]; - const nesting = FolderNode.getFileNesting(this.config, this.root.descendants, this.relativePath === undefined); + const nesting = FolderNode.getFileNesting(this.explorer.config, this.root.descendants, this.relativePath === undefined); if (nesting !== GitExplorerFilesLayout.List) { children = []; for (const folder of Objects.values(this.root.children)) { if (folder.value === undefined) { - children.push(new FolderNode(this.repoPath, folder.name, folder.relativePath, folder, this.config)); + children.push(new FolderNode(this.repoPath, folder.name, folder.relativePath, folder, this.explorer)); continue; } diff --git a/src/views/gitExplorer.ts b/src/views/gitExplorer.ts index c575814..4f5e6fb 100644 --- a/src/views/gitExplorer.ts +++ b/src/views/gitExplorer.ts @@ -1,9 +1,9 @@ 'use strict'; -import { Arrays, Functions, Objects } from '../system'; -import { commands, Disposable, Event, EventEmitter, ExtensionContext, TextDocumentShowOptions, TextEditor, TreeDataProvider, TreeItem, Uri, window, workspace } from 'vscode'; +import { Arrays, Functions } from '../system'; +import { commands, ConfigurationChangeEvent, Disposable, Event, EventEmitter, ExtensionContext, TextDocumentShowOptions, TextEditor, TreeDataProvider, TreeItem, Uri, window, workspace } from 'vscode'; import { Commands, DiffWithCommandArgs, DiffWithCommandArgsRevision, DiffWithPreviousCommandArgs, DiffWithWorkingCommandArgs, openEditor, OpenFileInRemoteCommandArgs } from '../commands'; import { UriComparer } from '../comparers'; -import { ExtensionKey, GitExplorerFilesLayout, IConfig } from '../configuration'; +import { configuration, ExtensionKey, GitExplorerFilesLayout, IGitExplorerConfig } from '../configuration'; import { CommandContext, GlyphChars, setCommandContext, WorkspaceState } from '../constants'; import { BranchHistoryNode, CommitFileNode, CommitNode, ExplorerNode, HistoryNode, MessageNode, RepositoriesNode, RepositoryNode, StashNode } from './explorerNodes'; import { GitChangeEvent, GitChangeReason, GitService, GitUri } from '../gitService'; @@ -38,7 +38,7 @@ export interface RefreshNodeCommandArgs { export class GitExplorer implements TreeDataProvider { - private _config: IConfig; + private _config: IGitExplorerConfig; private _root?: ExplorerNode; private _view: GitExplorerView | undefined; @@ -56,8 +56,8 @@ export class GitExplorer implements TreeDataProvider { public readonly context: ExtensionContext, public readonly git: GitService ) { - commands.registerCommand('gitlens.gitExplorer.setAutoRefreshToOn', () => this.setAutoRefresh(this.git.config.gitExplorer.autoRefresh, true), this); - commands.registerCommand('gitlens.gitExplorer.setAutoRefreshToOff', () => this.setAutoRefresh(this.git.config.gitExplorer.autoRefresh, false), this); + commands.registerCommand('gitlens.gitExplorer.setAutoRefreshToOn', () => this.setAutoRefresh(configuration.get(configuration.name('gitExplorer')('autoRefresh').value), true), this); + commands.registerCommand('gitlens.gitExplorer.setAutoRefreshToOff', () => this.setAutoRefresh(configuration.get(configuration.name('gitExplorer')('autoRefresh').value), false), this); commands.registerCommand('gitlens.gitExplorer.setFilesLayoutToAuto', () => this.setFilesLayout(GitExplorerFilesLayout.Auto), this); commands.registerCommand('gitlens.gitExplorer.setFilesLayoutToList', () => this.setFilesLayout(GitExplorerFilesLayout.List), this); commands.registerCommand('gitlens.gitExplorer.setFilesLayoutToTree', () => this.setFilesLayout(GitExplorerFilesLayout.Tree), this); @@ -76,27 +76,85 @@ export class GitExplorer implements TreeDataProvider { commands.registerCommand('gitlens.gitExplorer.openChangedFileRevisions', this.openChangedFileRevisions, this); commands.registerCommand('gitlens.gitExplorer.applyChanges', this.applyChanges, this); - const editorChangedFn = Functions.debounce(this.onActiveEditorChanged, 500); - context.subscriptions.push(window.onDidChangeActiveTextEditor(editorChangedFn, this)); + context.subscriptions.push( + window.onDidChangeActiveTextEditor(Functions.debounce(this.onActiveEditorChanged, 500), this), + window.onDidChangeVisibleTextEditors(Functions.debounce(this.onVisibleEditorsChanged, 500), this), + configuration.onDidChange(this.onConfigurationChanged, this) + ); + this.onConfigurationChanged(configuration.initializingChangeEvent); + } - const visibleEditorsChangedFn = Functions.debounce(this.onVisibleEditorsChanged, 500); - context.subscriptions.push(window.onDidChangeVisibleTextEditors(visibleEditorsChangedFn, this)); + private async onActiveEditorChanged(editor: TextEditor | undefined) { + if (this._view !== GitExplorerView.History) return; - context.subscriptions.push(workspace.onDidChangeConfiguration(this.onConfigurationChanged, this)); + const root = await this.getRootNode(editor); + if (!this.setRoot(root)) return; - this.onConfigurationChanged(); + this.refresh(RefreshReason.ActiveEditorChanged, root); } - get autoRefresh() { - return this._config.gitExplorer.autoRefresh && this.context.workspaceState.get(WorkspaceState.GitExplorerAutoRefresh, true); + private async onConfigurationChanged(e: ConfigurationChangeEvent) { + const initializing = configuration.initializing(e); + + const section = configuration.name('gitExplorer'); + if (!initializing && !configuration.changed(e, section.value)) return; + + const cfg = configuration.get(section.value); + + if (initializing || configuration.changed(e, section('autoRefresh').value)) { + this.setAutoRefresh(cfg.autoRefresh); + } + + if (initializing || configuration.changed(e, section('files')('layout').value)) { + setCommandContext(CommandContext.GitExplorerFilesLayout, cfg.files.layout); + } + + let view = cfg.view; + if (view === GitExplorerView.Auto) { + view = this.context.workspaceState.get(WorkspaceState.GitExplorerView, GitExplorerView.Repository); + } + + if (initializing) { + this._view = view; + setCommandContext(CommandContext.GitExplorerView, this._view); + + this.setRoot(await this.getRootNode(window.activeTextEditor)); + } + else { + this.reset(view); + } + + this._config = cfg; + } + + private onGitChanged(e: GitChangeEvent) { + if (this._root === undefined || this._view !== GitExplorerView.Repository || e.reason !== GitChangeReason.Repositories) return; + + this.clearRoot(); + + Logger.log(`GitExplorer[view=${this._view}].onGitChanged(${e.reason})`); + + this.refresh(RefreshReason.RepoChanged); + } + + private onVisibleEditorsChanged(editors: TextEditor[]) { + if (this._root === undefined || this._view !== GitExplorerView.History) return; + + // If we have no visible editors, or no trackable visible editors reset the view + if (editors.length === 0 || !editors.some(e => e.document && this.git.isTrackable(e.document.uri))) { + this.clearRoot(); + + this.refresh(RefreshReason.VisibleEditorsChanged); + } } - get config() { - return this._config.gitExplorer; + get autoRefresh() { + return configuration.get(configuration.name('gitExplorer')('autoRefresh').value) && + this.context.workspaceState.get(WorkspaceState.GitExplorerAutoRefresh, true); } - async getTreeItem(node: ExplorerNode): Promise { - return node.getTreeItem(); + get config(): IGitExplorerConfig { + return this._config; } private _loading: Promise | undefined; @@ -116,6 +174,10 @@ export class GitExplorer implements TreeDataProvider { return node.getChildren(); } + async getTreeItem(node: ExplorerNode): Promise { + return node.getTreeItem(); + } + private async getRootNode(editor?: TextEditor): Promise { switch (this._view) { case GitExplorerView.History: { @@ -156,61 +218,6 @@ export class GitExplorer implements TreeDataProvider { return new HistoryNode(uri, repo, this); } - private async onActiveEditorChanged(editor: TextEditor | undefined) { - if (this._view !== GitExplorerView.History) return; - - const root = await this.getRootNode(editor); - if (!this.setRoot(root)) return; - - this.refresh(RefreshReason.ActiveEditorChanged, root); - } - - private onConfigurationChanged() { - const cfg = workspace.getConfiguration().get(ExtensionKey)!; - - const changed = !Objects.areEquivalent(cfg.gitExplorer, this._config && this._config.gitExplorer); - - if (cfg.gitExplorer.autoRefresh !== (this._config && this._config.gitExplorer.autoRefresh)) { - this.setAutoRefresh(cfg.gitExplorer.autoRefresh); - } - - if (cfg.gitExplorer.files.layout !== (this._config && this._config.gitExplorer.files.layout)) { - setCommandContext(CommandContext.GitExplorerFilesLayout, cfg.gitExplorer.files.layout); - } - - this._config = cfg; - - if (changed) { - let view = cfg.gitExplorer.view; - if (view === GitExplorerView.Auto) { - view = this.context.workspaceState.get(WorkspaceState.GitExplorerView, GitExplorerView.Repository); - } - - this.reset(view); - } - } - - private onGitChanged(e: GitChangeEvent) { - if (this._root === undefined || this._view !== GitExplorerView.Repository || e.reason !== GitChangeReason.Repositories) return; - - this.clearRoot(); - - Logger.log(`GitExplorer[view=${this._view}].onGitChanged(${e.reason})`); - - this.refresh(RefreshReason.RepoChanged); - } - - private onVisibleEditorsChanged(editors: TextEditor[]) { - if (this._root === undefined || this._view !== GitExplorerView.History) return; - - // If we have no visible editors, or no trackable visible editors reset the view - if (editors.length === 0 || !editors.some(e => e.document && this.git.isTrackable(e.document.uri))) { - this.clearRoot(); - - this.refresh(RefreshReason.VisibleEditorsChanged); - } - } - async refresh(reason: RefreshReason | undefined, root?: ExplorerNode) { if (reason === undefined) { reason = RefreshReason.Command; @@ -270,7 +277,7 @@ export class GitExplorer implements TreeDataProvider { setView(view: GitExplorerView) { if (this._view === view) return; - if (this._config.gitExplorer.view === GitExplorerView.Auto) { + if (configuration.get(configuration.name('gitExplorer')('view').value) === GitExplorerView.Auto) { this.context.workspaceState.update(WorkspaceState.GitExplorerView, view); } @@ -407,6 +414,6 @@ export class GitExplorer implements TreeDataProvider { } private async setFilesLayout(layout: GitExplorerFilesLayout) { - await workspace.getConfiguration(ExtensionKey).update('gitExplorer.files.layout', layout, true); + return workspace.getConfiguration(ExtensionKey).update(configuration.name('gitExplorer')('files')('layout').value, layout, true); } } \ No newline at end of file diff --git a/src/views/remoteNode.ts b/src/views/remoteNode.ts index 11fc5bb..91891c1 100644 --- a/src/views/remoteNode.ts +++ b/src/views/remoteNode.ts @@ -1,10 +1,11 @@ 'use strict'; import { Iterables } from '../system'; -import { ExtensionContext, TreeItem, TreeItemCollapsibleState } from 'vscode'; +import { TreeItem, TreeItemCollapsibleState } from 'vscode'; import { BranchHistoryNode } from './branchHistoryNode'; import { GlyphChars } from '../constants'; import { ExplorerNode, ResourceType } from './explorerNode'; -import { GitRemote, GitRemoteType, GitService, GitUri } from '../gitService'; +import { GitExplorer } from './gitExplorer'; +import { GitRemote, GitRemoteType, GitUri, Repository } from '../gitService'; export class RemoteNode extends ExplorerNode { @@ -13,18 +14,18 @@ export class RemoteNode extends ExplorerNode { constructor( public readonly remote: GitRemote, uri: GitUri, - protected readonly context: ExtensionContext, - protected readonly git: GitService + private readonly repo: Repository, + private readonly explorer: GitExplorer ) { super(uri); } async getChildren(): Promise { - const branches = await this.git.getBranches(this.uri.repoPath!); + const branches = await this.repo.getBranches(); if (branches === undefined) return []; branches.sort((a, b) => a.name.localeCompare(b.name)); - return [...Iterables.filterMap(branches, b => !b.remote || !b.name.startsWith(this.remote.name) ? undefined : new BranchHistoryNode(b, this.uri, this.context, this.git))]; + return [...Iterables.filterMap(branches, b => !b.remote || !b.name.startsWith(this.remote.name) ? undefined : new BranchHistoryNode(b, this.uri, this.explorer))]; } getTreeItem(): TreeItem { diff --git a/src/views/remotesNode.ts b/src/views/remotesNode.ts index 5bd7b26..9a19f7a 100644 --- a/src/views/remotesNode.ts +++ b/src/views/remotesNode.ts @@ -1,8 +1,9 @@ 'use strict'; import { Iterables } from '../system'; -import { ExtensionContext, TreeItem, TreeItemCollapsibleState } from 'vscode'; +import { TreeItem, TreeItemCollapsibleState } from 'vscode'; import { ExplorerNode, MessageNode, ResourceType } from './explorerNode'; -import { GitService, GitUri } from '../gitService'; +import { GitExplorer } from './gitExplorer'; +import { GitUri, Repository } from '../gitService'; import { RemoteNode } from './remoteNode'; export class RemotesNode extends ExplorerNode { @@ -11,18 +12,18 @@ export class RemotesNode extends ExplorerNode { constructor( uri: GitUri, - protected readonly context: ExtensionContext, - protected readonly git: GitService + private readonly repo: Repository, + private readonly explorer: GitExplorer ) { super(uri); } async getChildren(): Promise { - const remotes = await this.git.getRemotes(this.uri.repoPath!); + const remotes = await this.repo.getRemotes(); if (remotes === undefined || remotes.length === 0) return [new MessageNode('No remotes configured')]; remotes.sort((a, b) => a.name.localeCompare(b.name)); - return [...Iterables.map(remotes, r => new RemoteNode(r, this.uri, this.context, this.git))]; + return [...Iterables.map(remotes, r => new RemoteNode(r, this.uri, this.repo, this.explorer))]; } getTreeItem(): TreeItem { @@ -30,8 +31,8 @@ export class RemotesNode extends ExplorerNode { item.contextValue = this.resourceType; item.iconPath = { - dark: this.context.asAbsolutePath('images/dark/icon-remote.svg'), - light: this.context.asAbsolutePath('images/light/icon-remote.svg') + dark: this.explorer.context.asAbsolutePath('images/dark/icon-remote.svg'), + light: this.explorer.context.asAbsolutePath('images/light/icon-remote.svg') }; return item; diff --git a/src/views/repositoryNode.ts b/src/views/repositoryNode.ts index d2f5648..1a10df9 100644 --- a/src/views/repositoryNode.ts +++ b/src/views/repositoryNode.ts @@ -28,9 +28,9 @@ export class RepositoryNode extends ExplorerNode { this.children = [ new StatusNode(this.uri, this.repo, this, this.explorer), - new BranchesNode(this.uri, this.explorer.context, this.explorer.git), - new RemotesNode(this.uri, this.explorer.context, this.explorer.git), - new StashesNode(this.uri, this.explorer.context, this.explorer.git) + new BranchesNode(this.uri, this.repo, this.explorer), + new RemotesNode(this.uri, this.repo, this.explorer), + new StashesNode(this.uri, this.repo, this.explorer) ]; return this.children; } diff --git a/src/views/stashFileNode.ts b/src/views/stashFileNode.ts index 824eaf6..179f7a0 100644 --- a/src/views/stashFileNode.ts +++ b/src/views/stashFileNode.ts @@ -1,8 +1,8 @@ 'use strict'; -import { ExtensionContext } from 'vscode'; -import { ResourceType } from './explorerNode'; -import { GitLogCommit, GitService, IGitStatusFile } from '../gitService'; import { CommitFileNode, CommitFileNodeDisplayAs } from './commitFileNode'; +import { ResourceType } from './explorerNode'; +import { GitExplorer } from './gitExplorer'; +import { GitLogCommit, IGitStatusFile } from '../gitService'; export class StashFileNode extends CommitFileNode { @@ -11,17 +11,16 @@ export class StashFileNode extends CommitFileNode { constructor( status: IGitStatusFile, commit: GitLogCommit, - context: ExtensionContext, - git: GitService + explorer: GitExplorer ) { - super(status, commit, context, git, CommitFileNodeDisplayAs.File); + super(status, commit, explorer, CommitFileNodeDisplayAs.File); } protected getCommitTemplate() { - return this.git.config.gitExplorer.stashFormat; + return this.explorer.config.stashFormat; } protected getCommitFileTemplate() { - return this.git.config.gitExplorer.stashFileFormat; + return this.explorer.config.stashFileFormat; } } \ No newline at end of file diff --git a/src/views/stashNode.ts b/src/views/stashNode.ts index a93a51f..40e1f01 100644 --- a/src/views/stashNode.ts +++ b/src/views/stashNode.ts @@ -1,8 +1,9 @@ 'use strict'; import { Iterables } from '../system'; -import { ExtensionContext, TreeItem, TreeItemCollapsibleState } from 'vscode'; +import { TreeItem, TreeItemCollapsibleState } from 'vscode'; import { ExplorerNode, ResourceType } from './explorerNode'; -import { CommitFormatter, GitService, GitStashCommit, GitUri, ICommitFormatOptions } from '../gitService'; +import { GitExplorer } from './gitExplorer'; +import { CommitFormatter, GitStashCommit, GitUri, ICommitFormatOptions } from '../gitService'; import { StashFileNode } from './stashFileNode'; export class StashNode extends ExplorerNode { @@ -11,8 +12,7 @@ export class StashNode extends ExplorerNode { constructor( public readonly commit: GitStashCommit, - protected readonly context: ExtensionContext, - protected readonly git: GitService + private readonly explorer: GitExplorer ) { super(new GitUri(commit.uri, commit)); } @@ -21,7 +21,7 @@ export class StashNode extends ExplorerNode { const statuses = (this.commit as GitStashCommit).fileStatuses; // Check for any untracked files -- since git doesn't return them via `git stash list` :( - const log = await this.git.getLogForRepo(this.commit.repoPath, `${(this.commit as GitStashCommit).stashName}^3`, 1); + const log = await this.explorer.git.getLogForRepo(this.commit.repoPath, `${(this.commit as GitStashCommit).stashName}^3`, 1); if (log !== undefined) { const commit = Iterables.first(log.commits.values()); if (commit !== undefined && commit.fileStatuses.length !== 0) { @@ -31,15 +31,15 @@ export class StashNode extends ExplorerNode { } } - const children = statuses.map(s => new StashFileNode(s, this.commit.toFileCommit(s), this.context, this.git)); + const children = statuses.map(s => new StashFileNode(s, this.commit.toFileCommit(s), this.explorer)); children.sort((a, b) => a.label!.localeCompare(b.label!)); return children; } getTreeItem(): TreeItem { - const item = new TreeItem(CommitFormatter.fromTemplate(this.git.config.gitExplorer.stashFormat, this.commit, { + const item = new TreeItem(CommitFormatter.fromTemplate(this.explorer.config.stashFormat, this.commit, { truncateMessageAtNewLine: true, - dataFormat: this.git.config.defaultDateFormat + dataFormat: this.explorer.git.config.defaultDateFormat } as ICommitFormatOptions), TreeItemCollapsibleState.Collapsed); item.contextValue = this.resourceType; return item; diff --git a/src/views/stashesNode.ts b/src/views/stashesNode.ts index e8b64eb..b862929 100644 --- a/src/views/stashesNode.ts +++ b/src/views/stashesNode.ts @@ -1,8 +1,9 @@ 'use strict'; import { Iterables } from '../system'; -import { ExtensionContext, TreeItem, TreeItemCollapsibleState } from 'vscode'; +import { TreeItem, TreeItemCollapsibleState } from 'vscode'; import { ExplorerNode, MessageNode, ResourceType } from './explorerNode'; -import { GitService, GitUri } from '../gitService'; +import { GitExplorer } from './gitExplorer'; +import { GitUri, Repository } from '../gitService'; import { StashNode } from './stashNode'; export class StashesNode extends ExplorerNode { @@ -11,17 +12,17 @@ export class StashesNode extends ExplorerNode { constructor( uri: GitUri, - protected readonly context: ExtensionContext, - protected readonly git: GitService + private readonly repo: Repository, + private readonly explorer: GitExplorer ) { super(uri); } async getChildren(): Promise { - const stash = await this.git.getStashList(this.uri.repoPath!); + const stash = await this.repo.getStashList(); if (stash === undefined) return [new MessageNode('No stashed changes')]; - return [...Iterables.map(stash.commits.values(), c => new StashNode(c, this.context, this.git))]; + return [...Iterables.map(stash.commits.values(), c => new StashNode(c, this.explorer))]; } getTreeItem(): TreeItem { @@ -29,8 +30,8 @@ export class StashesNode extends ExplorerNode { item.contextValue = this.resourceType; item.iconPath = { - dark: this.context.asAbsolutePath('images/dark/icon-stash.svg'), - light: this.context.asAbsolutePath('images/light/icon-stash.svg') + dark: this.explorer.context.asAbsolutePath('images/dark/icon-stash.svg'), + light: this.explorer.context.asAbsolutePath('images/light/icon-stash.svg') }; return item; diff --git a/src/views/statusFileCommitsNode.ts b/src/views/statusFileCommitsNode.ts index 5658b4a..afce748 100644 --- a/src/views/statusFileCommitsNode.ts +++ b/src/views/statusFileCommitsNode.ts @@ -1,9 +1,10 @@ 'use strict'; -import { Command, ExtensionContext, TreeItem, TreeItemCollapsibleState, Uri } from 'vscode'; +import { Command, TreeItem, TreeItemCollapsibleState, Uri } from 'vscode'; import { Commands, DiffWithPreviousCommandArgs } from '../commands'; import { CommitFileNode, CommitFileNodeDisplayAs } from './commitFileNode'; import { ExplorerNode, ResourceType } from './explorerNode'; -import { getGitStatusIcon, GitBranch, GitLogCommit, GitService, GitUri, IGitStatusFile, IGitStatusFileWithCommit, IStatusFormatOptions, StatusFileFormatter } from '../gitService'; +import { GitExplorer } from './gitExplorer'; +import { getGitStatusIcon, GitBranch, GitLogCommit, GitUri, IGitStatusFile, IGitStatusFileWithCommit, IStatusFormatOptions, StatusFileFormatter } from '../gitService'; import * as path from 'path'; export class StatusFileCommitsNode extends ExplorerNode { @@ -14,15 +15,14 @@ export class StatusFileCommitsNode extends ExplorerNode { public readonly repoPath: string, public readonly status: IGitStatusFile, public readonly commits: GitLogCommit[], - protected readonly context: ExtensionContext, - protected readonly git: GitService, + private readonly explorer: GitExplorer, public readonly branch?: GitBranch ) { super(new GitUri(Uri.file(path.resolve(repoPath, status.fileName)), { repoPath: repoPath, fileName: status.fileName, sha: 'HEAD' })); } async getChildren(): Promise { - return this.commits.map(c => new CommitFileNode(this.status, c, this.context, this.git, CommitFileNodeDisplayAs.Commit, this.branch)); + return this.commits.map(c => new CommitFileNode(this.status, c, this.explorer, CommitFileNodeDisplayAs.Commit, this.branch)); } async getTreeItem(): Promise { @@ -31,8 +31,8 @@ export class StatusFileCommitsNode extends ExplorerNode { const icon = getGitStatusIcon(this.status.status); item.iconPath = { - dark: this.context.asAbsolutePath(path.join('images', 'dark', icon)), - light: this.context.asAbsolutePath(path.join('images', 'light', icon)) + dark: this.explorer.context.asAbsolutePath(path.join('images', 'dark', icon)), + light: this.explorer.context.asAbsolutePath(path.join('images', 'light', icon)) }; if (this.commits.length === 1 && this.commits[0].isUncommitted) { @@ -58,7 +58,7 @@ export class StatusFileCommitsNode extends ExplorerNode { private _label: string | undefined; get label() { if (this._label === undefined) { - this._label = StatusFileFormatter.fromTemplate(this.git.config.gitExplorer.statusFileFormat, + this._label = StatusFileFormatter.fromTemplate(this.explorer.config.statusFileFormat, { ...this.status, commit: this.commit } as IGitStatusFileWithCommit, { relativePath: this.relativePath } as IStatusFormatOptions); } diff --git a/src/views/statusFilesNode.ts b/src/views/statusFilesNode.ts index c3de00a..ccb28aa 100644 --- a/src/views/statusFilesNode.ts +++ b/src/views/statusFilesNode.ts @@ -1,9 +1,10 @@ 'use strict'; import { Arrays, Iterables, Objects } from '../system'; -import { ExtensionContext, TreeItem, TreeItemCollapsibleState, Uri } from 'vscode'; +import { TreeItem, TreeItemCollapsibleState, Uri } from 'vscode'; import { GitExplorerFilesLayout } from '../configuration'; import { ExplorerNode, ResourceType, ShowAllNode } from './explorerNode'; import { FolderNode, IFileExplorerNode } from './folderNode'; +import { GitExplorer } from './gitExplorer'; import { GitBranch, GitCommitType, GitLog, GitLogCommit, GitService, GitStatus, GitUri, IGitStatusFileWithCommit } from '../gitService'; import { StatusFileCommitsNode } from './statusFileCommitsNode'; import * as path from 'path'; @@ -18,8 +19,7 @@ export class StatusFilesNode extends ExplorerNode { constructor( public readonly status: GitStatus, public readonly range: string | undefined, - protected readonly context: ExtensionContext, - protected readonly git: GitService, + private readonly explorer: GitExplorer, public readonly branch?: GitBranch ) { super(new GitUri(Uri.file(status.repoPath), { repoPath: status.repoPath, fileName: status.repoPath })); @@ -33,7 +33,7 @@ export class StatusFilesNode extends ExplorerNode { let log: GitLog | undefined; if (this.range !== undefined) { - log = await this.git.getLogForRepo(repoPath, this.range, this.maxCount); + log = await this.explorer.git.getLogForRepo(repoPath, this.range, this.maxCount); if (log !== undefined) { statuses = Array.from(Iterables.flatMap(log.commits.values(), c => { return c.fileStatuses.map(s => { @@ -56,14 +56,14 @@ export class StatusFilesNode extends ExplorerNode { const groups = Arrays.groupBy(statuses, s => s.fileName); let children: IFileExplorerNode[] = [ - ...Iterables.map(Objects.values(groups), statuses => new StatusFileCommitsNode(repoPath, statuses[statuses.length - 1], statuses.map(s => s.commit), this.context, this.git, this.branch)) + ...Iterables.map(Objects.values(groups), statuses => new StatusFileCommitsNode(repoPath, statuses[statuses.length - 1], statuses.map(s => s.commit), this.explorer, this.branch)) ]; - if (this.git.config.gitExplorer.files.layout !== GitExplorerFilesLayout.List) { + if (this.explorer.config.files.layout !== GitExplorerFilesLayout.List) { const hierarchy = Arrays.makeHierarchical(children, n => n.uri.getRelativePath().split('/'), - (...paths: string[]) => GitService.normalizePath(path.join(...paths)), this.git.config.gitExplorer.files.compact); + (...paths: string[]) => GitService.normalizePath(path.join(...paths)), this.explorer.config.files.compact); - const root = new FolderNode(repoPath, '', undefined, hierarchy, this.git.config.gitExplorer); + const root = new FolderNode(repoPath, '', undefined, hierarchy, this.explorer); children = await root.getChildren() as IFileExplorerNode[]; } else { @@ -71,7 +71,7 @@ export class StatusFilesNode extends ExplorerNode { } if (log !== undefined && log.truncated) { - (children as (IFileExplorerNode | ShowAllNode)[]).push(new ShowAllNode('Show All Changes', this, this.context)); + (children as (IFileExplorerNode | ShowAllNode)[]).push(new ShowAllNode('Show All Changes', this, this.explorer.context)); } return children; } @@ -80,7 +80,7 @@ export class StatusFilesNode extends ExplorerNode { let files = (this.status.files !== undefined && this.includeWorkingTree) ? this.status.files.length : 0; if (this.status.upstream !== undefined) { - const stats = await this.git.getChangedFilesCount(this.repoPath, `${this.status.upstream}...`); + const stats = await this.explorer.git.getChangedFilesCount(this.repoPath, `${this.status.upstream}...`); if (stats !== undefined) { files += stats.files; } @@ -90,14 +90,14 @@ export class StatusFilesNode extends ExplorerNode { const item = new TreeItem(label, TreeItemCollapsibleState.Collapsed); item.contextValue = this.resourceType; item.iconPath = { - dark: this.context.asAbsolutePath(`images/dark/icon-diff.svg`), - light: this.context.asAbsolutePath(`images/light/icon-diff.svg`) + dark: this.explorer.context.asAbsolutePath(`images/dark/icon-diff.svg`), + light: this.explorer.context.asAbsolutePath(`images/light/icon-diff.svg`) }; return item; } private get includeWorkingTree(): boolean { - return this.git.config.gitExplorer.includeWorkingTree; + return this.explorer.config.includeWorkingTree; } } \ No newline at end of file diff --git a/src/views/statusNode.ts b/src/views/statusNode.ts index ccd7e4e..7753e78 100644 --- a/src/views/statusNode.ts +++ b/src/views/statusNode.ts @@ -28,18 +28,18 @@ export class StatusNode extends ExplorerNode { if (status === undefined) return this.children; if (status.state.behind) { - this.children.push(new StatusUpstreamNode(status, 'behind', this.explorer.context, this.explorer.git)); + this.children.push(new StatusUpstreamNode(status, 'behind', this.explorer)); } if (status.state.ahead) { - this.children.push(new StatusUpstreamNode(status, 'ahead', this.explorer.context, this.explorer.git)); + this.children.push(new StatusUpstreamNode(status, 'ahead', this.explorer)); } if (status.state.ahead || (status.files.length !== 0 && this.includeWorkingTree)) { const range = status.upstream ? `${status.upstream}..${status.branch}` : undefined; - this.children.push(new StatusFilesNode(status, range, this.explorer.context, this.explorer.git)); + this.children.push(new StatusFilesNode(status, range, this.explorer)); } return this.children; diff --git a/src/views/statusUpstreamNode.ts b/src/views/statusUpstreamNode.ts index a94303a..49724d8 100644 --- a/src/views/statusUpstreamNode.ts +++ b/src/views/statusUpstreamNode.ts @@ -1,9 +1,10 @@ 'use strict'; import { Iterables } from '../system'; -import { ExtensionContext, TreeItem, TreeItemCollapsibleState, Uri } from 'vscode'; +import { TreeItem, TreeItemCollapsibleState, Uri } from 'vscode'; import { CommitNode } from './commitNode'; import { ExplorerNode, ResourceType } from './explorerNode'; -import { GitService, GitStatus, GitUri } from '../gitService'; +import { GitExplorer } from './gitExplorer'; +import { GitStatus, GitUri } from '../gitService'; export class StatusUpstreamNode extends ExplorerNode { @@ -12,8 +13,7 @@ export class StatusUpstreamNode extends ExplorerNode { constructor( public readonly status: GitStatus, public readonly direction: 'ahead' | 'behind', - protected readonly context: ExtensionContext, - protected readonly git: GitService + private readonly explorer: GitExplorer ) { super(new GitUri(Uri.file(status.repoPath), { repoPath: status.repoPath, fileName: status.repoPath })); } @@ -23,22 +23,22 @@ export class StatusUpstreamNode extends ExplorerNode { ? `${this.status.upstream}..${this.status.branch}` : `${this.status.branch}..${this.status.upstream}`; - let log = await this.git.getLogForRepo(this.uri.repoPath!, range, 0); + let log = await this.explorer.git.getLogForRepo(this.uri.repoPath!, range, 0); if (log === undefined) return []; - if (this.direction !== 'ahead') return [...Iterables.map(log.commits.values(), c => new CommitNode(c, this.context, this.git))]; + 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 this.git.getLogForRepo(this.uri.repoPath!, commit.sha, 2); + log = await this.explorer.git.getLogForRepo(this.uri.repoPath!, commit.sha, 2); if (log !== undefined) { commits[commits.length - 1] = Iterables.first(log.commits.values()); } } - return [...Iterables.map(commits, c => new CommitNode(c, this.context, this.git))]; + return [...Iterables.map(commits, c => new CommitNode(c, this.explorer))]; } async getTreeItem(): Promise { @@ -50,8 +50,8 @@ export class StatusUpstreamNode extends ExplorerNode { item.contextValue = this.resourceType; item.iconPath = { - dark: this.context.asAbsolutePath(`images/dark/icon-${this.direction === 'ahead' ? 'upload' : 'download'}.svg`), - light: this.context.asAbsolutePath(`images/light/icon-${this.direction === 'ahead' ? 'upload' : 'download'}.svg`) + dark: this.explorer.context.asAbsolutePath(`images/dark/icon-${this.direction === 'ahead' ? 'upload' : 'download'}.svg`), + light: this.explorer.context.asAbsolutePath(`images/light/icon-${this.direction === 'ahead' ? 'upload' : 'download'}.svg`) }; return item;