NEW
Adds rich tooltip details to the GitLens explorer and GitLens Results view
diff --git a/src/views/branchFolderNode.ts b/src/views/branchFolderNode.ts
index 6195f32..a8538f8 100644
--- a/src/views/branchFolderNode.ts
+++ b/src/views/branchFolderNode.ts
@@ -1,6 +1,6 @@
'use strict';
import { Arrays, Objects } from '../system';
-import { TreeItem, TreeItemCollapsibleState } from 'vscode';
+import { ThemeIcon, TreeItem, TreeItemCollapsibleState } from 'vscode';
import { BranchNode } from './branchNode';
// import { Container } from '../container';
import { Explorer, ExplorerNode, ResourceType } from './explorerNode';
@@ -37,7 +37,7 @@ export class BranchFolderNode extends ExplorerNode {
async getTreeItem(): Promise {
const item = new TreeItem(this.label, TreeItemCollapsibleState.Collapsed);
item.contextValue = ResourceType.Folder;
- item.resourceUri = this.explorer.folderResourceUri;
+ item.iconPath = ThemeIcon.Folder;
item.tooltip = this.label;
return item;
}
diff --git a/src/views/explorerNode.ts b/src/views/explorerNode.ts
index 771347c..939e448 100644
--- a/src/views/explorerNode.ts
+++ b/src/views/explorerNode.ts
@@ -5,6 +5,7 @@ import { Container } from '../container';
import { RefreshNodeCommandArgs } from './explorerCommands';
import { GitUri } from '../gitService';
import { GitExplorer } from './gitExplorer';
+import { HistoryExplorer } from './historyExplorer';
import { ResultsExplorer } from './resultsExplorer';
export interface NamedRef {
@@ -61,7 +62,7 @@ export enum ResourceType {
Tags = 'gitlens:tags'
}
-export type Explorer = GitExplorer | ResultsExplorer;
+export type Explorer = GitExplorer | HistoryExplorer | ResultsExplorer;
// let id = 0;
diff --git a/src/views/fileHistoryNode.ts b/src/views/fileHistoryNode.ts
index 4e11150..6c1325c 100644
--- a/src/views/fileHistoryNode.ts
+++ b/src/views/fileHistoryNode.ts
@@ -1,10 +1,9 @@
'use strict';
import { Iterables } from '../system';
-import { Disposable, TreeItem, TreeItemCollapsibleState } from 'vscode';
+import { TreeItem, TreeItemCollapsibleState } from 'vscode';
import { CommitFileNode, CommitFileNodeDisplayAs } from './commitFileNode';
import { Container } from '../container';
-import { ExplorerNode, MessageNode, ResourceType } from './explorerNode';
-import { GitExplorer } from './gitExplorer';
+import { Explorer, ExplorerNode, MessageNode, ResourceType } from './explorerNode';
import { GitCommitType, GitLogCommit, GitService, GitUri, Repository, RepositoryChange, RepositoryChangeEvent } from '../gitService';
import { Logger } from '../logger';
@@ -13,7 +12,7 @@ export class FileHistoryNode extends ExplorerNode {
constructor(
uri: GitUri,
private readonly repo: Repository,
- private readonly explorer: GitExplorer
+ private readonly explorer: Explorer
) {
super(uri);
}
@@ -85,36 +84,13 @@ export class FileHistoryNode extends ExplorerNode {
}
private updateSubscription() {
- // We only need to subscribe if auto-refresh is enabled, because if it becomes enabled we will be refreshed
- if (this.explorer.autoRefresh) {
- this.disposable = this.disposable || Disposable.from(
- this.explorer.onDidChangeAutoRefresh(this.onAutoRefreshChanged, this),
- this.repo.onDidChange(this.onRepoChanged, this)
- // Container.gitContextTracker.onDidChangeBlameability(this.onBlameabilityChanged, this)
- );
- }
- else if (this.disposable !== undefined) {
- this.disposable.dispose();
- this.disposable = undefined;
- }
- }
-
- private onAutoRefreshChanged() {
- this.updateSubscription();
+ this.disposable = this.disposable || this.repo.onDidChange(this.onRepoChanged, this);
}
- // private onBlameabilityChanged(e: BlameabilityChangeEvent) {
- // if (!e.blameable || e.reason !== BlameabilityChangeReason.DocumentChanged) return;
-
- // // Logger.log(`RepositoryNode.onBlameabilityChanged(${e.reason}); triggering node refresh`);
-
- // this.explorer.refreshNode(this);
- // }
-
private onRepoChanged(e: RepositoryChangeEvent) {
if (!e.changed(RepositoryChange.Repository)) return;
- Logger.log(`RepositoryNode.onRepoChanged(${e.changes.join()}); triggering node refresh`);
+ Logger.log(`FileHistoryNode.onRepoChanged(${e.changes.join()}); triggering node refresh`);
this.explorer.refreshNode(this);
}
diff --git a/src/views/folderNode.ts b/src/views/folderNode.ts
index 93e5832..bd9d700 100644
--- a/src/views/folderNode.ts
+++ b/src/views/folderNode.ts
@@ -1,6 +1,6 @@
'use strict';
import { Arrays, Objects } from '../system';
-import { TreeItem, TreeItemCollapsibleState } from 'vscode';
+import { ThemeIcon, TreeItem, TreeItemCollapsibleState } from 'vscode';
import { ExplorerFilesLayout, IExplorersFilesConfig } from '../configuration';
import { Explorer, ExplorerNode, ResourceType } from './explorerNode';
import { GitUri } from '../gitService';
@@ -63,7 +63,7 @@ export class FolderNode extends ExplorerNode {
// TODO: Change this to expanded once https://github.com/Microsoft/vscode/issues/30918 is fixed
const item = new TreeItem(this.label, TreeItemCollapsibleState.Collapsed);
item.contextValue = ResourceType.Folder;
- item.resourceUri = this.explorer.folderResourceUri;
+ item.iconPath = ThemeIcon.Folder;
item.tooltip = this.label;
return item;
}
diff --git a/src/views/gitExplorer.ts b/src/views/gitExplorer.ts
index 0d6864e..46619f7 100644
--- a/src/views/gitExplorer.ts
+++ b/src/views/gitExplorer.ts
@@ -1,12 +1,12 @@
'use strict';
import { Functions } from '../system';
-import { commands, ConfigurationChangeEvent, ConfigurationTarget, Disposable, Event, EventEmitter, TextDocumentShowOptions, TextEditor, TreeDataProvider, TreeItem, Uri, window, workspace } from 'vscode';
+import { commands, ConfigurationChangeEvent, ConfigurationTarget, Disposable, Event, EventEmitter, TextDocumentShowOptions, TextEditor, TreeDataProvider, TreeItem, Uri, window } from 'vscode';
import { UriComparer } from '../comparers';
import { configuration, ExplorerFilesLayout, GitExplorerView, IExplorersConfig, IGitExplorerConfig } from '../configuration';
import { CommandContext, GlyphChars, setCommandContext, WorkspaceState } from '../constants';
import { Container } from '../container';
import { RefreshNodeCommandArgs } from './explorerCommands';
-import { ExplorerNode, HistoryNode, MessageNode, RefreshReason, RepositoriesNode, RepositoryNode } from './explorerNodes';
+import { Explorer, ExplorerNode, HistoryNode, MessageNode, RefreshReason, RepositoriesNode, RepositoryNode } from './explorerNodes';
import { GitUri } from '../gitService';
import { Logger } from '../logger';
@@ -21,7 +21,6 @@ export class GitExplorer extends Disposable implements TreeDataProvider();
public get onDidChangeAutoRefresh(): Event {
@@ -45,11 +44,13 @@ export class GitExplorer extends Disposable implements TreeDataProvider this.setAutoRefresh(Container.config.gitExplorer.autoRefresh, true), this);
commands.registerCommand('gitlens.gitExplorer.setAutoRefreshToOff', () => this.setAutoRefresh(Container.config.gitExplorer.autoRefresh, false), this);
- commands.registerCommand('gitlens.gitExplorer.setRenameFollowingOn', () => this.setRenameFollowing(true), this);
- commands.registerCommand('gitlens.gitExplorer.setRenameFollowingOff', () => this.setRenameFollowing(false), this);
+ commands.registerCommand('gitlens.gitExplorer.setRenameFollowingOn', () => GitExplorer.setRenameFollowing(true), this);
+ commands.registerCommand('gitlens.gitExplorer.setRenameFollowingOff', () => GitExplorer.setRenameFollowing(false), this);
commands.registerCommand('gitlens.gitExplorer.switchToHistoryView', () => this.switchTo(GitExplorerView.History), this);
commands.registerCommand('gitlens.gitExplorer.switchToRepositoryView', () => this.switchTo(GitExplorerView.Repository), this);
+ commands.registerCommand('gitlens.gitExplorer.undockHistory', this.undockHistory, this);
+
Container.context.subscriptions.push(
window.onDidChangeActiveTextEditor(Functions.debounce(this.onActiveEditorChanged, 500), this),
window.onDidChangeVisibleTextEditors(Functions.debounce(this.onVisibleEditorsChanged, 500), this),
@@ -62,15 +63,6 @@ export class GitExplorer extends Disposable implements TreeDataProvider e.document && Container.git.isTrackable(e.document.uri))) {
@@ -141,11 +151,12 @@ export class GitExplorer extends Disposable implements TreeDataProvider | undefined;
@@ -157,7 +168,7 @@ export class GitExplorer extends Disposable implements TreeDataProvider {
- switch (this._view) {
- case GitExplorerView.History: {
- const promise = this.getHistoryNode(editor || window.activeTextEditor);
- this._loading = promise.then(_ => Functions.wait(0));
- return promise;
- }
- default: {
- const promise = Container.git.getRepositories();
- this._loading = promise.then(_ => Functions.wait(0));
-
- const repositories = [...await promise];
- if (repositories.length === 0) return undefined; // new MessageNode('No repositories found');
-
- if (repositories.length === 1) {
- const repo = repositories[0];
- return new RepositoryNode(GitUri.fromRepoPath(repo.path), repo, this, true);
- }
-
- return new RepositoriesNode(repositories, this);
- }
- }
- }
-
- private async getHistoryNode(editor: TextEditor | undefined): Promise {
- // If we have no active editor, or no visible editors, or no trackable visible editors reset the view
- if (editor === undefined || window.visibleTextEditors.length === 0 || !window.visibleTextEditors.some(e => e.document && Container.git.isTrackable(e.document.uri))) return undefined;
- // If we do have a visible trackable editor, don't change from the last state (avoids issues when focus switches to the problems/output/debug console panes)
- if (editor.document === undefined || !Container.git.isTrackable(editor.document.uri)) return this._root;
-
- const uri = await GitUri.fromUri(editor.document.uri);
-
- const repo = await Container.git.getRepository(uri);
- if (repo === undefined) return undefined;
-
- if (UriComparer.equals(uri, this._root && this._root.uri)) return this._root;
-
- return new HistoryNode(uri, repo, this);
- }
-
getQualifiedCommand(command: string) {
return `gitlens.gitExplorer.${command}`;
}
@@ -218,9 +189,9 @@ export class GitExplorer extends Disposable implements TreeDataProvider(WorkspaceState.GitExplorerAutoRefresh, true);
+ }
+ else {
+ toggled = workspaceEnabled;
+ await Container.context.workspaceState.update(WorkspaceState.GitExplorerAutoRefresh, workspaceEnabled);
- private setRoot(root: ExplorerNode | undefined): boolean {
- if (this._root === root) return false;
+ this._onDidChangeAutoRefresh.fire();
+ }
- if (this._root !== undefined) {
- this._root.dispose();
+ if (workspaceEnabled) {
+ this._autoRefreshDisposable = Container.git.onDidChangeRepositories(this.onRepositoriesChanged, this);
+ Container.context.subscriptions.push(this._autoRefreshDisposable);
+ }
}
- this._root = root;
- return true;
+ setCommandContext(CommandContext.GitExplorerAutoRefresh, enabled && workspaceEnabled);
+
+ if (toggled) {
+ this.refresh(RefreshReason.AutoRefreshChanged);
+ }
}
setView(view: GitExplorerView) {
- if (this._view === view) return;
+ if (this.view === view) return;
if (Container.config.gitExplorer.view === GitExplorerView.Auto) {
Container.context.workspaceState.update(WorkspaceState.GitExplorerView, view);
}
- this._view = view;
- setCommandContext(CommandContext.GitExplorerView, this._view);
+ this.view = view;
+ setCommandContext(CommandContext.GitExplorerView, this.view);
if (view !== GitExplorerView.Repository) {
Container.git.stopWatchingFileSystem();
@@ -291,45 +273,86 @@ export class GitExplorer extends Disposable implements TreeDataProvider(WorkspaceState.GitExplorerAutoRefresh, true);
- }
- else {
- toggled = workspaceEnabled;
- await Container.context.workspaceState.update(WorkspaceState.GitExplorerAutoRefresh, workspaceEnabled);
+ this._root.dispose();
+ this._root = undefined;
+ }
- this._onDidChangeAutoRefresh.fire();
+ private async getRootNode(editor?: TextEditor): Promise {
+ switch (this.view) {
+ case GitExplorerView.History: {
+ const promise = this.getHistoryNode(editor || window.activeTextEditor);
+ this._loading = promise.then(_ => Functions.wait(0));
+ return promise;
}
+ default: {
+ const promise = Container.git.getRepositories();
+ this._loading = promise.then(_ => Functions.wait(0));
- if (workspaceEnabled) {
- this._autoRefreshDisposable = Container.git.onDidChangeRepositories(this.onRepositoriesChanged, this);
- Container.context.subscriptions.push(this._autoRefreshDisposable);
+ const repositories = [...await promise];
+ if (repositories.length === 0) return undefined; // new MessageNode('No repositories found');
+
+ if (repositories.length === 1) {
+ const repo = repositories[0];
+ return new RepositoryNode(GitUri.fromRepoPath(repo.path), repo, this, true);
+ }
+
+ return new RepositoriesNode(repositories, this);
}
}
+ }
- setCommandContext(CommandContext.GitExplorerAutoRefresh, enabled && workspaceEnabled);
+ private async getHistoryNode(editor: TextEditor | undefined): Promise {
+ return GitExplorer.getHistoryNode(this, editor, this._root);
+ }
- if (toggled) {
- this.refresh(RefreshReason.AutoRefreshChanged);
+ private async setFilesLayout(layout: ExplorerFilesLayout) {
+ return configuration.update(configuration.name('gitExplorer')('files')('layout').value, layout, ConfigurationTarget.Global);
+ }
+
+ private setRoot(root: ExplorerNode | undefined): boolean {
+ if (this._root === root) return false;
+
+ if (this._root !== undefined) {
+ this._root.dispose();
}
+
+ this._root = root;
+ return true;
+ }
+
+ private async undockHistory(switchView: boolean = true) {
+ Container.historyExplorer.undock(switchView);
+ }
+
+ static async getHistoryNode(explorer: Explorer, editor: TextEditor | undefined, root: ExplorerNode | undefined): Promise {
+ // If we have no active editor, or no visible editors, or no trackable visible editors reset the view
+ if (editor === undefined || window.visibleTextEditors.length === 0 || !window.visibleTextEditors.some(e => e.document && Container.git.isTrackable(e.document.uri))) return undefined;
+ // If we do have a visible trackable editor, don't change from the last state (avoids issues when focus switches to the problems/output/debug console panes)
+ if (editor.document === undefined || !Container.git.isTrackable(editor.document.uri)) return root;
+
+ const uri = await GitUri.fromUri(editor.document.uri);
+
+ const repo = await Container.git.getRepository(uri);
+ if (repo === undefined) return undefined;
+
+ if (UriComparer.equals(uri, root && root.uri)) return root;
+
+ return new HistoryNode(uri, repo, explorer);
}
- setRenameFollowing(enabled: boolean) {
+ static setRenameFollowing(enabled: boolean) {
configuration.updateEffective(configuration.name('advanced')('fileHistoryFollowsRenames').value, enabled);
}
}
\ No newline at end of file
diff --git a/src/views/historyExplorer.ts b/src/views/historyExplorer.ts
new file mode 100644
index 0000000..116a522
--- /dev/null
+++ b/src/views/historyExplorer.ts
@@ -0,0 +1,178 @@
+'use strict';
+import { Functions } from '../system';
+import { commands, ConfigurationChangeEvent, Disposable, Event, EventEmitter, TextEditor, TreeDataProvider, TreeItem, window } from 'vscode';
+import { configuration, GitExplorerView, IExplorersConfig } from '../configuration';
+import { CommandContext, GlyphChars, setCommandContext } from '../constants';
+import { Container } from '../container';
+import { RefreshNodeCommandArgs } from './explorerCommands';
+import { ExplorerNode, MessageNode, RefreshReason } from './explorerNodes';
+import { GitExplorer } from './gitExplorer';
+import { Logger } from '../logger';
+
+export * from './explorerNodes';
+
+export class HistoryExplorer extends Disposable implements TreeDataProvider {
+
+ private _disposable: Disposable | undefined;
+ private _root?: ExplorerNode;
+
+ private _onDidChangeTreeData = new EventEmitter();
+ public get onDidChangeTreeData(): Event {
+ return this._onDidChangeTreeData.event;
+ }
+
+ constructor() {
+ super(() => this.dispose());
+
+ Container.explorerCommands;
+ commands.registerCommand('gitlens.historyExplorer.refresh', this.refresh, this);
+ commands.registerCommand('gitlens.historyExplorer.refreshNode', this.refreshNode, this);
+ commands.registerCommand('gitlens.historyExplorer.close', () => this.dock(false), this);
+ commands.registerCommand('gitlens.historyExplorer.dock', this.dock, this);
+
+ commands.registerCommand('gitlens.historyExplorer.setRenameFollowingOn', () => GitExplorer.setRenameFollowing(true), this);
+ commands.registerCommand('gitlens.historyExplorer.setRenameFollowingOff', () => GitExplorer.setRenameFollowing(false), this);
+
+ Container.context.subscriptions.push(
+ window.onDidChangeActiveTextEditor(Functions.debounce(this.onActiveEditorChanged, 500), this),
+ window.onDidChangeVisibleTextEditors(Functions.debounce(this.onVisibleEditorsChanged, 500), this),
+ configuration.onDidChange(this.onConfigurationChanged, this)
+ );
+ this.onConfigurationChanged(configuration.initializingChangeEvent);
+ }
+
+ dispose() {
+ this._disposable && this._disposable.dispose();
+ }
+
+ private async onConfigurationChanged(e: ConfigurationChangeEvent) {
+ const initializing = configuration.initializing(e);
+
+ if (!initializing &&
+ !configuration.changed(e, configuration.name('historyExplorer').value) &&
+ !configuration.changed(e, configuration.name('explorers').value) &&
+ !configuration.changed(e, configuration.name('defaultGravatarsStyle').value) &&
+ !configuration.changed(e, configuration.name('advanced')('fileHistoryFollowsRenames').value)) return;
+
+ if (initializing || configuration.changed(e, configuration.name('historyExplorer')('enabled').value)) {
+ if (Container.config.historyExplorer.enabled) {
+ this.undock(!initializing);
+ }
+ else {
+ this.dock(!initializing);
+ }
+ }
+
+ if (!initializing && this._root === undefined) {
+ this.refresh(RefreshReason.ConfigurationChanged);
+ }
+
+ if (initializing) {
+ this.setRoot(await this.getRootNode(window.activeTextEditor));
+
+ this._disposable = window.registerTreeDataProvider('gitlens.historyExplorer', this);
+ }
+ }
+
+ private async onActiveEditorChanged(editor: TextEditor | undefined) {
+ const root = await this.getRootNode(editor);
+ if (!this.setRoot(root)) return;
+
+ this.refresh(RefreshReason.ActiveEditorChanged, root);
+ }
+
+ private onVisibleEditorsChanged(editors: TextEditor[]) {
+ if (this._root === undefined) return;
+
+ // If we have no visible editors, or no trackable visible editors reset the view
+ if (editors.length === 0 || !editors.some(e => e.document && Container.git.isTrackable(e.document.uri))) {
+ this.clearRoot();
+
+ this.refresh(RefreshReason.VisibleEditorsChanged);
+ }
+ }
+
+ get config(): IExplorersConfig {
+ return { ...Container.config.explorers };
+ }
+
+ async getChildren(node?: ExplorerNode): Promise {
+ if (this._root === undefined) return [new MessageNode(`No active file ${GlyphChars.Dash} no history to show`)];
+
+ if (node === undefined) return this._root.getChildren();
+ return node.getChildren();
+ }
+
+ async getTreeItem(node: ExplorerNode): Promise {
+ return node.getTreeItem();
+ }
+
+ async dock(switchView: boolean = true) {
+ if (switchView) {
+ await Container.gitExplorer.switchTo(GitExplorerView.History);
+ }
+ setCommandContext(CommandContext.HistoryExplorer, false);
+ configuration.updateEffective(configuration.name('historyExplorer')('enabled').value, false);
+ }
+
+ getQualifiedCommand(command: string) {
+ return `gitlens.historyExplorer.${command}`;
+ }
+
+ async refresh(reason?: RefreshReason, root?: ExplorerNode) {
+ if (reason === undefined) {
+ reason = RefreshReason.Command;
+ }
+
+ Logger.log(`HistoryExplorer.refresh`, `reason='${reason}'`);
+
+ if (this._root === undefined || root === undefined) {
+ this.clearRoot();
+ this.setRoot(await this.getRootNode(window.activeTextEditor));
+ }
+
+ this._onDidChangeTreeData.fire();
+ }
+
+ refreshNode(node: ExplorerNode, args?: RefreshNodeCommandArgs) {
+ Logger.log(`HistoryExplorer.refreshNode(${(node as any).id})`);
+
+ if (args !== undefined && node.supportsPaging) {
+ node.maxCount = args.maxCount;
+ }
+ node.refresh();
+
+ // Since a root node won't actually refresh, force everything
+ this._onDidChangeTreeData.fire(this._root === node ? undefined : node);
+ }
+
+ async undock(switchView: boolean = true) {
+ if (switchView) {
+ await Container.gitExplorer.switchTo(GitExplorerView.Repository);
+ }
+ setCommandContext(CommandContext.HistoryExplorer, true);
+ configuration.updateEffective(configuration.name('historyExplorer')('enabled').value, true);
+ }
+
+ private clearRoot() {
+ if (this._root === undefined) return;
+
+ this._root.dispose();
+ this._root = undefined;
+ }
+
+ private async getRootNode(editor: TextEditor | undefined): Promise {
+ return GitExplorer.getHistoryNode(this, editor, this._root);
+ }
+
+ private setRoot(root: ExplorerNode | undefined): boolean {
+ if (this._root === root) return false;
+
+ if (this._root !== undefined) {
+ this._root.dispose();
+ }
+
+ this._root = root;
+ return true;
+ }
+}
\ No newline at end of file
diff --git a/src/views/historyNode.ts b/src/views/historyNode.ts
index f02394f..b351973 100644
--- a/src/views/historyNode.ts
+++ b/src/views/historyNode.ts
@@ -1,9 +1,8 @@
'use strict';
import { TreeItem, TreeItemCollapsibleState } from 'vscode';
import { Container } from '../container';
-import { ExplorerNode, ResourceType } from './explorerNode';
+import { Explorer, ExplorerNode, ResourceType } from './explorerNode';
import { FileHistoryNode } from './fileHistoryNode';
-import { GitExplorer } from './gitExplorer';
import { GitUri, Repository } from '../gitService';
export class HistoryNode extends ExplorerNode {
@@ -11,7 +10,7 @@ export class HistoryNode extends ExplorerNode {
constructor(
uri: GitUri,
private readonly repo: Repository,
- private readonly explorer: GitExplorer
+ private readonly explorer: Explorer
) {
super(uri);
}
diff --git a/src/views/resultsExplorer.ts b/src/views/resultsExplorer.ts
index d050780..22ef4f7 100644
--- a/src/views/resultsExplorer.ts
+++ b/src/views/resultsExplorer.ts
@@ -1,6 +1,6 @@
'use strict';
import { Functions, Strings } from '../system';
-import { commands, ConfigurationChangeEvent, ConfigurationTarget, Disposable, Event, EventEmitter, TreeDataProvider, TreeItem, Uri, window, workspace } from 'vscode';
+import { commands, ConfigurationChangeEvent, ConfigurationTarget, Disposable, Event, EventEmitter, TreeDataProvider, TreeItem, window } from 'vscode';
import { configuration, ExplorerFilesLayout, IExplorersConfig, IResultsExplorerConfig } from '../configuration';
import { CommandContext, GlyphChars, setCommandContext, WorkspaceState } from '../constants';
import { Container } from '../container';
@@ -71,13 +71,6 @@ export class ResultsExplorer extends Disposable implements TreeDataProvider(WorkspaceState.ResultsExplorerKeepResults, false);
}
@@ -113,7 +106,7 @@ export class ResultsExplorer extends Disposable implements TreeDataProvider