提交 9ae0fd36 编写于 作者: E Eric Amodio

Enhances timeline - commands, timestamp, etc

Adds contributable commands to timeline items
Adds right-aligned timestamp to timeline items
Adds Open Changes to Git timeline items
Adds Copy Commit ID to Git timeline items
Adds Copy Commit Message to Git timeline items
上级 69b30f6b
...@@ -447,6 +447,22 @@ ...@@ -447,6 +447,22 @@
"command": "git.stashDrop", "command": "git.stashDrop",
"title": "%command.stashDrop%", "title": "%command.stashDrop%",
"category": "Git" "category": "Git"
},
{
"command": "git.timeline.openDiff",
"title": "%command.timelineOpenDiff%",
"icon": "$(compare-changes)",
"category": "Git"
},
{
"command": "git.timeline.copyCommitId",
"title": "%command.timelineCopyCommitId%",
"category": "Git"
},
{
"command": "git.timeline.copyCommitMessage",
"title": "%command.timelineCopyCommitMessage%",
"category": "Git"
} }
], ],
"menus": { "menus": {
...@@ -718,6 +734,18 @@ ...@@ -718,6 +734,18 @@
{ {
"command": "git.stashDrop", "command": "git.stashDrop",
"when": "config.git.enabled && gitOpenRepositoryCount != 0" "when": "config.git.enabled && gitOpenRepositoryCount != 0"
},
{
"command": "git.timeline.openDiff",
"when": "false"
},
{
"command": "git.timeline.copyCommitId",
"when": "false"
},
{
"command": "git.timeline.copyCommitMessage",
"when": "false"
} }
], ],
"scm/title": [ "scm/title": [
...@@ -1248,6 +1276,28 @@ ...@@ -1248,6 +1276,28 @@
"command": "git.revertChange", "command": "git.revertChange",
"when": "originalResourceScheme == git" "when": "originalResourceScheme == git"
} }
],
"timeline/item/context": [
{
"command": "git.timeline.openDiff",
"group": "inline",
"when": "timelineItem =~ /git:file\\b/"
},
{
"command": "git.timeline.openDiff",
"group": "1_timeline",
"when": "timelineItem =~ /git:file\\b/"
},
{
"command": "git.timeline.copyCommitId",
"group": "2_timeline@1",
"when": "timelineItem =~ /git:file:commit\\b/"
},
{
"command": "git.timeline.copyCommitMessage",
"group": "2_timeline@2",
"when": "timelineItem =~ /git:file:commit\\b/"
}
] ]
}, },
"configuration": { "configuration": {
...@@ -1779,10 +1829,10 @@ ...@@ -1779,10 +1829,10 @@
"@types/file-type": "^5.2.1", "@types/file-type": "^5.2.1",
"@types/mocha": "2.2.43", "@types/mocha": "2.2.43",
"@types/node": "^12.11.7", "@types/node": "^12.11.7",
"@types/vscode": "^1.42",
"@types/which": "^1.0.28", "@types/which": "^1.0.28",
"mocha": "^3.2.0", "mocha": "^3.2.0",
"mocha-junit-reporter": "^1.23.3", "mocha-junit-reporter": "^1.23.3",
"mocha-multi-reporters": "^1.1.7", "mocha-multi-reporters": "^1.1.7"
"vscode": "^1.1.36"
} }
} }
...@@ -70,6 +70,9 @@ ...@@ -70,6 +70,9 @@
"command.stashApply": "Apply Stash...", "command.stashApply": "Apply Stash...",
"command.stashApplyLatest": "Apply Latest Stash", "command.stashApplyLatest": "Apply Latest Stash",
"command.stashDrop": "Drop Stash...", "command.stashDrop": "Drop Stash...",
"command.timelineOpenDiff": "Open Changes",
"command.timelineCopyCommitId": "Copy Commit ID",
"command.timelineCopyCommitMessage": "Copy Commit Message",
"config.enabled": "Whether git is enabled.", "config.enabled": "Whether git is enabled.",
"config.path": "Path and filename of the git executable, e.g. `C:\\Program Files\\Git\\bin\\git.exe` (Windows).", "config.path": "Path and filename of the git executable, e.g. `C:\\Program Files\\Git\\bin\\git.exe` (Windows).",
"config.autoRepositoryDetection": "Configures when repositories should be automatically detected.", "config.autoRepositoryDetection": "Configures when repositories should be automatically detected.",
......
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
import { lstat, Stats } from 'fs'; import { lstat, Stats } from 'fs';
import * as os from 'os'; import * as os from 'os';
import * as path from 'path'; import * as path from 'path';
import { commands, Disposable, LineChange, MessageOptions, OutputChannel, Position, ProgressLocation, QuickPickItem, Range, SourceControlResourceState, TextDocumentShowOptions, TextEditor, Uri, ViewColumn, window, workspace, WorkspaceEdit, WorkspaceFolder } from 'vscode'; import { commands, Disposable, LineChange, MessageOptions, OutputChannel, Position, ProgressLocation, QuickPickItem, Range, SourceControlResourceState, TextDocumentShowOptions, TextEditor, Uri, ViewColumn, window, workspace, WorkspaceEdit, WorkspaceFolder, TimelineItem, env } from 'vscode';
import TelemetryReporter from 'vscode-extension-telemetry'; import TelemetryReporter from 'vscode-extension-telemetry';
import * as nls from 'vscode-nls'; import * as nls from 'vscode-nls';
import { Branch, GitErrorCodes, Ref, RefType, Status, CommitOptions } from './api/git'; import { Branch, GitErrorCodes, Ref, RefType, Status, CommitOptions } from './api/git';
...@@ -17,6 +17,7 @@ import { applyLineChanges, getModifiedRange, intersectDiffWithRange, invertLineC ...@@ -17,6 +17,7 @@ import { applyLineChanges, getModifiedRange, intersectDiffWithRange, invertLineC
import { fromGitUri, toGitUri, isGitUri } from './uri'; import { fromGitUri, toGitUri, isGitUri } from './uri';
import { grep, isDescendant, pathEquals } from './util'; import { grep, isDescendant, pathEquals } from './util';
import { Log, LogLevel } from './log'; import { Log, LogLevel } from './log';
import { GitTimelineItem } from './timelineProvider';
const localize = nls.loadMessageBundle(); const localize = nls.loadMessageBundle();
...@@ -2331,23 +2332,47 @@ export class CommandCenter { ...@@ -2331,23 +2332,47 @@ export class CommandCenter {
return result && result.stash; return result && result.stash;
} }
@command('git.openDiff', { repository: false }) @command('git.timeline.openDiff', { repository: false })
async openDiff(uri: Uri, lhs: string, rhs: string) { async timelineOpenDiff(item: TimelineItem, uri: Uri | undefined, _source: string) {
// eslint-disable-next-line eqeqeq
if (uri == null || !GitTimelineItem.is(item)) {
return undefined;
}
const basename = path.basename(uri.fsPath); const basename = path.basename(uri.fsPath);
let title; let title;
if ((lhs === 'HEAD' || lhs === '~') && rhs === '') { if ((item.previousRef === 'HEAD' || item.previousRef === '~') && item.ref === '') {
title = `${basename} (Working Tree)`; title = `${basename} (Working Tree)`;
} }
else if (lhs === 'HEAD' && rhs === '~') { else if (item.previousRef === 'HEAD' && item.ref === '~') {
title = `${basename} (Index)`; title = `${basename} (Index)`;
} else { } else {
title = `${basename} (${lhs.endsWith('^') ? `${lhs.substr(0, 8)}^` : lhs.substr(0, 8)}) \u27f7 ${basename} (${rhs.endsWith('^') ? `${rhs.substr(0, 8)}^` : rhs.substr(0, 8)})`; title = `${basename} (${item.shortPreviousRef}) \u27f7 ${basename} (${item.shortRef})`;
}
return commands.executeCommand('vscode.diff', toGitUri(uri, item.previousRef), item.ref === '' ? uri : toGitUri(uri, item.ref), title);
}
@command('git.timeline.copyCommitId', { repository: false })
async timelineCopyCommitId(item: TimelineItem, _uri: Uri | undefined, _source: string) {
if (!GitTimelineItem.is(item)) {
return;
} }
return commands.executeCommand('vscode.diff', toGitUri(uri, lhs), rhs === '' ? uri : toGitUri(uri, rhs), title); env.clipboard.writeText(item.ref);
} }
@command('git.timeline.copyCommitMessage', { repository: false })
async timelineCopyCommitMessage(item: TimelineItem, _uri: Uri | undefined, _source: string) {
if (!GitTimelineItem.is(item)) {
return;
}
env.clipboard.writeText(item.message);
}
private createCommand(id: string, key: string, method: Function, options: CommandOptions): (...args: any[]) => any { private createCommand(id: string, key: string, method: Function, options: CommandOptions): (...args: any[]) => any {
const result = (...args: any[]) => { const result = (...args: any[]) => {
let result: Promise<any>; let result: Promise<any>;
......
...@@ -5,7 +5,6 @@ ...@@ -5,7 +5,6 @@
import * as dayjs from 'dayjs'; import * as dayjs from 'dayjs';
import * as advancedFormat from 'dayjs/plugin/advancedFormat'; import * as advancedFormat from 'dayjs/plugin/advancedFormat';
import * as relativeTime from 'dayjs/plugin/relativeTime';
import { CancellationToken, Disposable, Event, EventEmitter, ThemeIcon, Timeline, TimelineChangeEvent, TimelineCursor, TimelineItem, TimelineProvider, Uri, workspace } from 'vscode'; import { CancellationToken, Disposable, Event, EventEmitter, ThemeIcon, Timeline, TimelineChangeEvent, TimelineCursor, TimelineItem, TimelineProvider, Uri, workspace } from 'vscode';
import { Model } from './model'; import { Model } from './model';
import { Repository } from './repository'; import { Repository } from './repository';
...@@ -13,11 +12,55 @@ import { debounce } from './decorators'; ...@@ -13,11 +12,55 @@ import { debounce } from './decorators';
import { Status } from './api/git'; import { Status } from './api/git';
dayjs.extend(advancedFormat); dayjs.extend(advancedFormat);
dayjs.extend(relativeTime);
// TODO[ECA]: Localize all the strings // TODO[ECA]: Localize all the strings
// TODO[ECA]: Localize or use a setting for date format // TODO[ECA]: Localize or use a setting for date format
export class GitTimelineItem extends TimelineItem {
static is(item: TimelineItem): item is GitTimelineItem {
return item instanceof GitTimelineItem;
}
readonly ref: string;
readonly previousRef: string;
readonly message: string;
constructor(
ref: string,
previousRef: string,
message: string,
timestamp: number,
id: string,
contextValue: string
) {
const index = message.indexOf('\n');
const label = index !== -1 ? `${message.substring(0, index)} \u2026` : message;
super(label, timestamp);
this.ref = ref;
this.previousRef = previousRef;
this.message = message;
this.id = id;
this.contextValue = contextValue;
}
get shortRef() {
return this.shortenRef(this.ref);
}
get shortPreviousRef() {
return this.shortenRef(this.previousRef);
}
private shortenRef(ref: string): string {
if (ref === '' || ref === '~' || ref === 'HEAD') {
return ref;
}
return ref.endsWith('^') ? `${ref.substr(0, 8)}^` : ref.substr(0, 8);
}
}
export class GitTimelineProvider implements TimelineProvider { export class GitTimelineProvider implements TimelineProvider {
private _onDidChange = new EventEmitter<TimelineChangeEvent>(); private _onDidChange = new EventEmitter<TimelineChangeEvent>();
get onDidChange(): Event<TimelineChangeEvent> { get onDidChange(): Event<TimelineChangeEvent> {
...@@ -72,25 +115,17 @@ export class GitTimelineProvider implements TimelineProvider { ...@@ -72,25 +115,17 @@ export class GitTimelineProvider implements TimelineProvider {
const commits = await repo.logFile(uri); const commits = await repo.logFile(uri);
let dateFormatter: dayjs.Dayjs; let dateFormatter: dayjs.Dayjs;
const items = commits.map<TimelineItem>(c => { const items = commits.map<GitTimelineItem>(c => {
let message = c.message;
const index = message.indexOf('\n');
if (index !== -1) {
message = `${message.substring(0, index)} \u2026`;
}
dateFormatter = dayjs(c.authorDate); dateFormatter = dayjs(c.authorDate);
const item = new TimelineItem(message, c.authorDate?.getTime() ?? 0); const item = new GitTimelineItem(c.hash, `${c.hash}^`, c.message, c.authorDate?.getTime() ?? 0, c.hash, 'git:file:commit');
item.id = c.hash;
item.iconPath = new (ThemeIcon as any)('git-commit'); item.iconPath = new (ThemeIcon as any)('git-commit');
item.description = `${dateFormatter.fromNow()} \u2022 ${c.authorName}`; item.description = c.authorName;
item.detail = `${c.authorName} (${c.authorEmail}) \u2014 ${c.hash.substr(0, 8)}\n${dateFormatter.fromNow()} (${dateFormatter.format('MMMM Do, YYYY h:mma')})\n\n${c.message}`; item.detail = `${c.authorName} (${c.authorEmail}) \u2014 ${c.hash.substr(0, 8)}\n${dateFormatter.format('MMMM Do, YYYY h:mma')}\n\n${c.message}`;
item.command = { item.command = {
title: 'Open Diff', title: 'Open Comparison',
command: 'git.openDiff', command: 'git.timeline.openDiff',
arguments: [uri, `${c.hash}^`, c.hash] arguments: [uri, this.id, item]
}; };
return item; return item;
...@@ -123,16 +158,15 @@ export class GitTimelineProvider implements TimelineProvider { ...@@ -123,16 +158,15 @@ export class GitTimelineProvider implements TimelineProvider {
break; break;
} }
const item = new TimelineItem('Staged Changes', date.getTime()); const item = new GitTimelineItem('~', 'HEAD', 'Staged Changes', date.getTime(), 'index', 'git:file:index');
item.id = 'index';
// TODO[ECA]: Replace with a better icon -- reflecting its status maybe? // TODO[ECA]: Replace with a better icon -- reflecting its status maybe?
item.iconPath = new (ThemeIcon as any)('git-commit'); item.iconPath = new (ThemeIcon as any)('git-commit');
item.description = `${dateFormatter.fromNow()} \u2022 You`; item.description = 'You';
item.detail = `You \u2014 Index\n${dateFormatter.fromNow()} (${dateFormatter.format('MMMM Do, YYYY h:mma')})\n${status}`; item.detail = `You \u2014 Index\n${dateFormatter.format('MMMM Do, YYYY h:mma')}\n${status}`;
item.command = { item.command = {
title: 'Open Comparison', title: 'Open Comparison',
command: 'git.openDiff', command: 'git.timeline.openDiff',
arguments: [uri, 'HEAD', '~'] arguments: [uri, this.id, item]
}; };
items.push(item); items.push(item);
...@@ -166,16 +200,15 @@ export class GitTimelineProvider implements TimelineProvider { ...@@ -166,16 +200,15 @@ export class GitTimelineProvider implements TimelineProvider {
break; break;
} }
const item = new TimelineItem('Uncommited Changes', date.getTime()); const item = new GitTimelineItem('', index ? '~' : 'HEAD', 'Uncommited Changes', date.getTime(), 'working', 'git:file:working');
item.id = 'working';
// TODO[ECA]: Replace with a better icon -- reflecting its status maybe? // TODO[ECA]: Replace with a better icon -- reflecting its status maybe?
item.iconPath = new (ThemeIcon as any)('git-commit'); item.iconPath = new (ThemeIcon as any)('git-commit');
item.description = `${dateFormatter.fromNow()} \u2022 You`; item.description = 'You';
item.detail = `You \u2014 Working Tree\n${dateFormatter.fromNow()} (${dateFormatter.format('MMMM Do, YYYY h:mma')})\n${status}`; item.detail = `You \u2014 Working Tree\n${dateFormatter.format('MMMM Do, YYYY h:mma')}\n${status}`;
item.command = { item.command = {
title: 'Open Comparison', title: 'Open Comparison',
command: 'git.openDiff', command: 'git.timeline.openDiff',
arguments: [uri, index ? '~' : 'HEAD', ''] arguments: [uri, this.id, item]
}; };
items.push(item); items.push(item);
...@@ -208,6 +241,6 @@ export class GitTimelineProvider implements TimelineProvider { ...@@ -208,6 +241,6 @@ export class GitTimelineProvider implements TimelineProvider {
@debounce(500) @debounce(500)
private fireChanged() { private fireChanged() {
this._onDidChange.fire(); this._onDidChange.fire({});
} }
} }
此差异已折叠。
...@@ -5,6 +5,53 @@ ...@@ -5,6 +5,53 @@
import { pad } from './strings'; import { pad } from './strings';
const minute = 60;
const hour = minute * 60;
const day = hour * 24;
const week = day * 7;
const month = day * 30;
const year = day * 365;
// TODO[ECA]: Localize strings
export function fromNow(date: number | Date) {
if (typeof date !== 'number') {
date = date.getTime();
}
const seconds = Math.round((new Date().getTime() - date) / 1000);
if (seconds < 30) {
return 'now';
}
let value: number;
let unit: string;
if (seconds < minute) {
value = seconds;
unit = 'sec';
} else if (seconds < hour) {
value = Math.floor(seconds / minute);
unit = 'min';
} else if (seconds < day) {
value = Math.floor(seconds / hour);
unit = 'hr';
} else if (seconds < week) {
value = Math.floor(seconds / day);
unit = 'day';
} else if (seconds < month) {
value = Math.floor(seconds / week);
unit = 'wk';
} else if (seconds < year) {
value = Math.floor(seconds / month);
unit = 'mo';
} else {
value = Math.floor(seconds / year);
unit = 'yr';
}
return `${value} ${unit}${value === 1 ? '' : 's'}`;
}
export function toLocalISOString(date: Date): string { export function toLocalISOString(date: Date): string {
return date.getFullYear() + return date.getFullYear() +
'-' + pad(date.getMonth() + 1, 2) + '-' + pad(date.getMonth() + 1, 2) +
......
...@@ -114,7 +114,9 @@ export class MenuId { ...@@ -114,7 +114,9 @@ export class MenuId {
static readonly CommentActions = new MenuId('CommentActions'); static readonly CommentActions = new MenuId('CommentActions');
static readonly BulkEditTitle = new MenuId('BulkEditTitle'); static readonly BulkEditTitle = new MenuId('BulkEditTitle');
static readonly BulkEditContext = new MenuId('BulkEditContext'); static readonly BulkEditContext = new MenuId('BulkEditContext');
static readonly TimelineItemContext = new MenuId('TimelineItemContext');
static readonly TimelineTitle = new MenuId('TimelineTitle');
static readonly TimelineTitleContext = new MenuId('TimelineTitleContext');
readonly id: number; readonly id: number;
readonly _debugName: string; readonly _debugName: string;
......
...@@ -39,8 +39,8 @@ export class MainThreadTimeline implements MainThreadTimelineShape { ...@@ -39,8 +39,8 @@ export class MainThreadTimeline implements MainThreadTimelineShape {
this._timelineService.registerTimelineProvider({ this._timelineService.registerTimelineProvider({
...provider, ...provider,
onDidChange: onDidChange.event, onDidChange: onDidChange.event,
provideTimeline(uri: URI, cursor: TimelineCursor, token: CancellationToken) { provideTimeline(uri: URI, cursor: TimelineCursor, token: CancellationToken, options?: { cacheResults?: boolean }) {
return proxy.$getTimeline(provider.id, uri, cursor, token); return proxy.$getTimeline(provider.id, uri, cursor, token, options);
}, },
dispose() { dispose() {
emitters.delete(provider.id); emitters.delete(provider.id);
......
...@@ -133,7 +133,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I ...@@ -133,7 +133,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
const extHostLabelService = rpcProtocol.set(ExtHostContext.ExtHostLabelService, new ExtHostLabelService(rpcProtocol)); const extHostLabelService = rpcProtocol.set(ExtHostContext.ExtHostLabelService, new ExtHostLabelService(rpcProtocol));
const extHostTheming = rpcProtocol.set(ExtHostContext.ExtHostTheming, new ExtHostTheming(rpcProtocol)); const extHostTheming = rpcProtocol.set(ExtHostContext.ExtHostTheming, new ExtHostTheming(rpcProtocol));
const extHostAuthentication = rpcProtocol.set(ExtHostContext.ExtHostAuthentication, new ExtHostAuthentication(rpcProtocol)); const extHostAuthentication = rpcProtocol.set(ExtHostContext.ExtHostAuthentication, new ExtHostAuthentication(rpcProtocol));
const extHostTimeline = rpcProtocol.set(ExtHostContext.ExtHostTimeline, new ExtHostTimeline(rpcProtocol)); const extHostTimeline = rpcProtocol.set(ExtHostContext.ExtHostTimeline, new ExtHostTimeline(rpcProtocol, extHostCommands));
// Check that no named customers are missing // Check that no named customers are missing
const expected: ProxyIdentifier<any>[] = values(ExtHostContext); const expected: ProxyIdentifier<any>[] = values(ExtHostContext);
......
...@@ -1457,7 +1457,7 @@ export interface ExtHostTunnelServiceShape { ...@@ -1457,7 +1457,7 @@ export interface ExtHostTunnelServiceShape {
} }
export interface ExtHostTimelineShape { export interface ExtHostTimelineShape {
$getTimeline(source: string, uri: UriComponents, cursor: TimelineCursor, token: CancellationToken): Promise<Timeline | undefined>; $getTimeline(source: string, uri: UriComponents, cursor: TimelineCursor, token: CancellationToken, options?: { cacheResults?: boolean }): Promise<Timeline | undefined>;
} }
// --- proxy identifiers // --- proxy identifiers
......
...@@ -7,55 +7,77 @@ import * as vscode from 'vscode'; ...@@ -7,55 +7,77 @@ import * as vscode from 'vscode';
import { UriComponents, URI } from 'vs/base/common/uri'; import { UriComponents, URI } from 'vs/base/common/uri';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { ExtHostTimelineShape, MainThreadTimelineShape, IMainContext, MainContext } from 'vs/workbench/api/common/extHost.protocol'; import { ExtHostTimelineShape, MainThreadTimelineShape, IMainContext, MainContext } from 'vs/workbench/api/common/extHost.protocol';
import { Timeline, TimelineCursor, TimelineItemWithSource, TimelineProvider } from 'vs/workbench/contrib/timeline/common/timeline'; import { Timeline, TimelineCursor, TimelineItem, TimelineProvider } from 'vs/workbench/contrib/timeline/common/timeline';
import { IDisposable, toDisposable, DisposableStore } from 'vs/base/common/lifecycle'; import { IDisposable, toDisposable, DisposableStore } from 'vs/base/common/lifecycle';
import { CancellationToken } from 'vs/base/common/cancellation'; import { CancellationToken } from 'vs/base/common/cancellation';
import { CommandsConverter } from 'vs/workbench/api/common/extHostCommands'; import { CommandsConverter, ExtHostCommands } from 'vs/workbench/api/common/extHostCommands';
import { ThemeIcon } from 'vs/workbench/api/common/extHostTypes'; import { ThemeIcon } from 'vs/workbench/api/common/extHostTypes';
import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
export interface IExtHostTimeline extends ExtHostTimelineShape { export interface IExtHostTimeline extends ExtHostTimelineShape {
readonly _serviceBrand: undefined; readonly _serviceBrand: undefined;
$getTimeline(id: string, uri: UriComponents, cursor: vscode.TimelineCursor, token: vscode.CancellationToken): Promise<Timeline | undefined>; $getTimeline(id: string, uri: UriComponents, cursor: vscode.TimelineCursor, token: vscode.CancellationToken, options?: { cacheResults?: boolean }): Promise<Timeline | undefined>;
} }
export const IExtHostTimeline = createDecorator<IExtHostTimeline>('IExtHostTimeline'); export const IExtHostTimeline = createDecorator<IExtHostTimeline>('IExtHostTimeline');
export class ExtHostTimeline implements IExtHostTimeline { export class ExtHostTimeline implements IExtHostTimeline {
private static handlePool = 0;
_serviceBrand: undefined; _serviceBrand: undefined;
private _proxy: MainThreadTimelineShape; private _proxy: MainThreadTimelineShape;
private _providers = new Map<string, TimelineProvider>(); private _providers = new Map<string, TimelineProvider>();
private _itemsBySourceByUriMap = new Map<string | undefined, Map<string, Map<string, vscode.TimelineItem>>>();
constructor( constructor(
mainContext: IMainContext, mainContext: IMainContext,
commands: ExtHostCommands,
) { ) {
this._proxy = mainContext.getProxy(MainContext.MainThreadTimeline); this._proxy = mainContext.getProxy(MainContext.MainThreadTimeline);
commands.registerArgumentProcessor({
processArgument: arg => {
if (arg && arg.$mid === 11) {
const uri = arg.uri === undefined ? undefined : URI.revive(arg.uri);
return this._itemsBySourceByUriMap.get(getUriKey(uri))?.get(arg.source)?.get(arg.handle);
}
return arg;
}
});
} }
async $getTimeline(id: string, uri: UriComponents, cursor: vscode.TimelineCursor, token: vscode.CancellationToken): Promise<Timeline | undefined> { async $getTimeline(id: string, uri: UriComponents, cursor: vscode.TimelineCursor, token: vscode.CancellationToken, options?: { cacheResults?: boolean }): Promise<Timeline | undefined> {
const provider = this._providers.get(id); const provider = this._providers.get(id);
return provider?.provideTimeline(URI.revive(uri), cursor, token); return provider?.provideTimeline(URI.revive(uri), cursor, token, options);
} }
registerTimelineProvider(scheme: string | string[], provider: vscode.TimelineProvider, extensionId: ExtensionIdentifier, commandConverter: CommandsConverter): IDisposable { registerTimelineProvider(scheme: string | string[], provider: vscode.TimelineProvider, _extensionId: ExtensionIdentifier, commandConverter: CommandsConverter): IDisposable {
const timelineDisposables = new DisposableStore(); const timelineDisposables = new DisposableStore();
const convertTimelineItem = this.convertTimelineItem(provider.id, commandConverter, timelineDisposables); const convertTimelineItem = this.convertTimelineItem(provider.id, commandConverter, timelineDisposables).bind(this);
let disposable: IDisposable | undefined; let disposable: IDisposable | undefined;
if (provider.onDidChange) { if (provider.onDidChange) {
disposable = provider.onDidChange(this.emitTimelineChangeEvent(provider.id), this); disposable = provider.onDidChange(this.emitTimelineChangeEvent(provider.id), this);
} }
const itemsBySourceByUriMap = this._itemsBySourceByUriMap;
return this.registerTimelineProviderCore({ return this.registerTimelineProviderCore({
...provider, ...provider,
scheme: scheme, scheme: scheme,
onDidChange: undefined, onDidChange: undefined,
async provideTimeline(uri: URI, cursor: TimelineCursor, token: CancellationToken) { async provideTimeline(uri: URI, cursor: TimelineCursor, token: CancellationToken, options?: { cacheResults?: boolean }) {
timelineDisposables.clear(); timelineDisposables.clear();
// For now, only allow the caching of a single Uri
if (options?.cacheResults && !itemsBySourceByUriMap.has(getUriKey(uri))) {
itemsBySourceByUriMap.clear();
}
const result = await provider.provideTimeline(uri, cursor, token); const result = await provider.provideTimeline(uri, cursor, token);
// Intentional == we don't know how a provider will respond // Intentional == we don't know how a provider will respond
// eslint-disable-next-line eqeqeq // eslint-disable-next-line eqeqeq
...@@ -63,10 +85,12 @@ export class ExtHostTimeline implements IExtHostTimeline { ...@@ -63,10 +85,12 @@ export class ExtHostTimeline implements IExtHostTimeline {
return undefined; return undefined;
} }
// TODO: Determine if we should cache dependent on who calls us (internal vs external)
const convertItem = convertTimelineItem(uri, options?.cacheResults ?? false);
return { return {
...result, ...result,
source: provider.id, source: provider.id,
items: result.items.map(convertTimelineItem) items: result.items.map(convertItem)
}; };
}, },
dispose() { dispose() {
...@@ -76,39 +100,72 @@ export class ExtHostTimeline implements IExtHostTimeline { ...@@ -76,39 +100,72 @@ export class ExtHostTimeline implements IExtHostTimeline {
}); });
} }
private convertTimelineItem(source: string, commandConverter: CommandsConverter, disposables: DisposableStore): (item: vscode.TimelineItem) => TimelineItemWithSource { private convertTimelineItem(source: string, commandConverter: CommandsConverter, disposables: DisposableStore) {
return (item: vscode.TimelineItem) => { return (uri: URI, cacheResults: boolean) => {
const { iconPath, ...props } = item; let itemsMap: Map<string, vscode.TimelineItem> | undefined;
if (cacheResults) {
const uriKey = getUriKey(uri);
let icon; let sourceMap = this._itemsBySourceByUriMap.get(uriKey);
let iconDark; if (sourceMap === undefined) {
let themeIcon; sourceMap = new Map();
if (item.iconPath) { this._itemsBySourceByUriMap.set(uriKey, sourceMap);
if (iconPath instanceof ThemeIcon) {
themeIcon = { id: iconPath.id };
} }
else if (URI.isUri(iconPath)) {
icon = iconPath; itemsMap = sourceMap.get(source);
iconDark = iconPath; if (itemsMap === undefined) {
} itemsMap = new Map();
else { sourceMap.set(source, itemsMap);
({ light: icon, dark: iconDark } = iconPath as { light: URI; dark: URI });
} }
} }
return { return (item: vscode.TimelineItem): TimelineItem => {
...props, const { iconPath, ...props } = item;
source: source,
command: item.command ? commandConverter.toInternal(item.command, disposables) : undefined, const handle = `${source}|${item.id ?? `${item.timestamp}-${ExtHostTimeline.handlePool++}`}`;
icon: icon, itemsMap?.set(handle, item);
iconDark: iconDark,
themeIcon: themeIcon let icon;
let iconDark;
let themeIcon;
if (item.iconPath) {
if (iconPath instanceof ThemeIcon) {
themeIcon = { id: iconPath.id };
}
else if (URI.isUri(iconPath)) {
icon = iconPath;
iconDark = iconPath;
}
else {
({ light: icon, dark: iconDark } = iconPath as { light: URI; dark: URI });
}
}
return {
...props,
handle: handle,
source: source,
command: item.command ? commandConverter.toInternal(item.command, disposables) : undefined,
icon: icon,
iconDark: iconDark,
themeIcon: themeIcon
};
}; };
}; };
} }
private emitTimelineChangeEvent(id: string) { private emitTimelineChangeEvent(id: string) {
return (e: vscode.TimelineChangeEvent) => { return (e: vscode.TimelineChangeEvent) => {
// Clear caches
if (e?.uri === undefined) {
for (const sourceMap of this._itemsBySourceByUriMap.values()) {
sourceMap.get(id)?.clear();
}
}
else {
this._itemsBySourceByUriMap.get(getUriKey(e.uri))?.clear();
}
this._proxy.$emitTimelineChangeEvent({ ...e, id: id }); this._proxy.$emitTimelineChangeEvent({ ...e, id: id });
}; };
} }
...@@ -129,9 +186,18 @@ export class ExtHostTimeline implements IExtHostTimeline { ...@@ -129,9 +186,18 @@ export class ExtHostTimeline implements IExtHostTimeline {
this._providers.set(provider.id, provider); this._providers.set(provider.id, provider);
return toDisposable(() => { return toDisposable(() => {
for (const sourceMap of this._itemsBySourceByUriMap.values()) {
sourceMap.get(provider.id)?.clear();
}
this._providers.delete(provider.id); this._providers.delete(provider.id);
this._proxy.$unregisterTimelineProvider(provider.id); this._proxy.$unregisterTimelineProvider(provider.id);
provider.dispose(); provider.dispose();
}); });
} }
} }
function getUriKey(uri: URI | undefined): string | undefined {
return uri?.toString();
}
...@@ -52,6 +52,8 @@ namespace schema { ...@@ -52,6 +52,8 @@ namespace schema {
case 'comments/comment/title': return MenuId.CommentTitle; case 'comments/comment/title': return MenuId.CommentTitle;
case 'comments/comment/context': return MenuId.CommentActions; case 'comments/comment/context': return MenuId.CommentActions;
case 'extension/context': return MenuId.ExtensionContext; case 'extension/context': return MenuId.ExtensionContext;
case 'timeline/title': return MenuId.TimelineTitle;
case 'timeline/item/context': return MenuId.TimelineItemContext;
} }
return undefined; return undefined;
...@@ -215,6 +217,16 @@ namespace schema { ...@@ -215,6 +217,16 @@ namespace schema {
type: 'array', type: 'array',
items: menuItem items: menuItem
}, },
'timeline/title': {
description: localize('view.timelineTitle', "The Timeline view title menu"),
type: 'array',
items: menuItem
},
'timeline/item/context': {
description: localize('view.timelineContext', "The Timeline view item context menu"),
type: 'array',
items: menuItem
},
} }
}; };
......
...@@ -13,3 +13,20 @@ ...@@ -13,3 +13,20 @@
position: absolute; position: absolute;
pointer-events: none; pointer-events: none;
} }
.timeline-tree-view .monaco-list .monaco-list-row .custom-view-tree-node-item .monaco-icon-label {
flex: 1;
text-overflow: ellipsis;
overflow: hidden;
}
.timeline-tree-view .monaco-list .monaco-list-row .custom-view-tree-node-item .timeline-timestamp-container {
margin-left: 2px;
margin-right: 4px;
text-overflow: ellipsis;
overflow: hidden;
}
.timeline-tree-view .monaco-list .monaco-list-row .custom-view-tree-node-item .timeline-timestamp-container .timeline-timestamp {
opacity: 0.5;
}
...@@ -8,11 +8,11 @@ import { localize } from 'vs/nls'; ...@@ -8,11 +8,11 @@ import { localize } from 'vs/nls';
import * as DOM from 'vs/base/browser/dom'; import * as DOM from 'vs/base/browser/dom';
import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { CancellationTokenSource } from 'vs/base/common/cancellation';
import { FuzzyScore, createMatches } from 'vs/base/common/filters'; import { FuzzyScore, createMatches } from 'vs/base/common/filters';
import { DisposableStore } from 'vs/base/common/lifecycle'; import { DisposableStore, IDisposable, Disposable } from 'vs/base/common/lifecycle';
import { URI } from 'vs/base/common/uri'; import { URI } from 'vs/base/common/uri';
import { IconLabel } from 'vs/base/browser/ui/iconLabel/iconLabel'; import { IconLabel } from 'vs/base/browser/ui/iconLabel/iconLabel';
import { IListVirtualDelegate, IIdentityProvider, IKeyboardNavigationLabelProvider } from 'vs/base/browser/ui/list/list'; import { IListVirtualDelegate, IIdentityProvider, IKeyboardNavigationLabelProvider } from 'vs/base/browser/ui/list/list';
import { ITreeNode, ITreeRenderer } from 'vs/base/browser/ui/tree/tree'; import { ITreeNode, ITreeRenderer, ITreeContextMenuEvent } from 'vs/base/browser/ui/tree/tree';
import { ViewPane, IViewPaneOptions } from 'vs/workbench/browser/parts/views/viewPaneContainer'; import { ViewPane, IViewPaneOptions } from 'vs/workbench/browser/parts/views/viewPaneContainer';
import { TreeResourceNavigator, WorkbenchObjectTree } from 'vs/platform/list/browser/listService'; import { TreeResourceNavigator, WorkbenchObjectTree } from 'vs/platform/list/browser/listService';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
...@@ -20,7 +20,7 @@ import { IContextMenuService } from 'vs/platform/contextview/browser/contextView ...@@ -20,7 +20,7 @@ import { IContextMenuService } from 'vs/platform/contextview/browser/contextView
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { TimelineItem, ITimelineService, TimelineChangeEvent, TimelineProvidersChangeEvent, TimelineRequest, TimelineItemWithSource } from 'vs/workbench/contrib/timeline/common/timeline'; import { ITimelineService, TimelineChangeEvent, TimelineProvidersChangeEvent, TimelineRequest, TimelineItem } from 'vs/workbench/contrib/timeline/common/timeline';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { SideBySideEditor, toResource } from 'vs/workbench/common/editor'; import { SideBySideEditor, toResource } from 'vs/workbench/common/editor';
import { ICommandService } from 'vs/platform/commands/common/commands'; import { ICommandService } from 'vs/platform/commands/common/commands';
...@@ -31,10 +31,20 @@ import { IProgressService } from 'vs/platform/progress/common/progress'; ...@@ -31,10 +31,20 @@ import { IProgressService } from 'vs/platform/progress/common/progress';
import { VIEWLET_ID } from 'vs/workbench/contrib/files/common/files'; import { VIEWLET_ID } from 'vs/workbench/contrib/files/common/files';
import { debounce } from 'vs/base/common/decorators'; import { debounce } from 'vs/base/common/decorators';
import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IOpenerService } from 'vs/platform/opener/common/opener';
import { IActionViewItemProvider, ActionBar, ActionViewItem } from 'vs/base/browser/ui/actionbar/actionbar';
import { IAction, ActionRunner } from 'vs/base/common/actions';
import { ContextAwareMenuEntryActionViewItem, createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem';
import { MenuItemAction, IMenuService, MenuId } from 'vs/platform/actions/common/actions';
import { fromNow } from 'vs/base/common/date';
// TODO[ECA]: Localize all the strings
type TreeElement = TimelineItem; type TreeElement = TimelineItem;
// TODO[ECA]: Localize all the strings interface TimelineActionContext {
uri: URI | undefined;
item: TreeElement;
}
export class TimelinePane extends ViewPane { export class TimelinePane extends ViewPane {
static readonly ID = 'timeline'; static readonly ID = 'timeline';
...@@ -44,10 +54,12 @@ export class TimelinePane extends ViewPane { ...@@ -44,10 +54,12 @@ export class TimelinePane extends ViewPane {
private _messageElement!: HTMLDivElement; private _messageElement!: HTMLDivElement;
private _treeElement!: HTMLDivElement; private _treeElement!: HTMLDivElement;
private _tree!: WorkbenchObjectTree<TreeElement, FuzzyScore>; private _tree!: WorkbenchObjectTree<TreeElement, FuzzyScore>;
private _treeRenderer: TimelineTreeRenderer | undefined;
private _menus: TimelineMenus;
private _visibilityDisposables: DisposableStore | undefined; private _visibilityDisposables: DisposableStore | undefined;
// private _excludedSources: Set<string> | undefined; // private _excludedSources: Set<string> | undefined;
private _items: TimelineItemWithSource[] = []; private _items: TimelineItem[] = [];
private _loadingMessageTimer: any | undefined; private _loadingMessageTimer: any | undefined;
private _pendingRequests = new Map<string, TimelineRequest>(); private _pendingRequests = new Map<string, TimelineRequest>();
private _uri: URI | undefined; private _uri: URI | undefined;
...@@ -67,7 +79,9 @@ export class TimelinePane extends ViewPane { ...@@ -67,7 +79,9 @@ export class TimelinePane extends ViewPane {
@IOpenerService openerService: IOpenerService, @IOpenerService openerService: IOpenerService,
@IThemeService themeService: IThemeService, @IThemeService themeService: IThemeService,
) { ) {
super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService); super({ ...options, titleMenuId: MenuId.TimelineTitle }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService);
this._menus = this._register(this.instantiationService.createInstance(TimelineMenus, this.id));
const scopedContextKeyService = this._register(this.contextKeyService.createScoped()); const scopedContextKeyService = this._register(this.contextKeyService.createScoped());
scopedContextKeyService.createKey('view', TimelinePane.ID); scopedContextKeyService.createKey('view', TimelinePane.ID);
...@@ -88,6 +102,7 @@ export class TimelinePane extends ViewPane { ...@@ -88,6 +102,7 @@ export class TimelinePane extends ViewPane {
} }
this._uri = uri; this._uri = uri;
this._treeRenderer?.setUri(uri);
this.loadTimeline(); this.loadTimeline();
} }
...@@ -187,7 +202,7 @@ export class TimelinePane extends ViewPane { ...@@ -187,7 +202,7 @@ export class TimelinePane extends ViewPane {
let request = this._pendingRequests.get(source); let request = this._pendingRequests.get(source);
request?.tokenSource.dispose(true); request?.tokenSource.dispose(true);
request = this.timelineService.getTimeline(source, this._uri, {}, new CancellationTokenSource())!; request = this.timelineService.getTimeline(source, this._uri, {}, new CancellationTokenSource(), { cacheResults: true })!;
this._pendingRequests.set(source, request); this._pendingRequests.set(source, request);
request.tokenSource.token.onCancellationRequested(() => this._pendingRequests.delete(source)); request.tokenSource.token.onCancellationRequested(() => this._pendingRequests.delete(source));
...@@ -211,7 +226,7 @@ export class TimelinePane extends ViewPane { ...@@ -211,7 +226,7 @@ export class TimelinePane extends ViewPane {
this.replaceItems(request.source, items); this.replaceItems(request.source, items);
} }
private replaceItems(source: string, items?: TimelineItemWithSource[]) { private replaceItems(source: string, items?: TimelineItem[]) {
const hasItems = this._items.length !== 0; const hasItems = this._items.length !== 0;
if (items?.length) { if (items?.length) {
...@@ -291,17 +306,20 @@ export class TimelinePane extends ViewPane { ...@@ -291,17 +306,20 @@ export class TimelinePane extends ViewPane {
// DOM.addClass(this._treeElement, 'show-file-icons'); // DOM.addClass(this._treeElement, 'show-file-icons');
container.appendChild(this._treeElement); container.appendChild(this._treeElement);
const renderer = this.instantiationService.createInstance(TimelineTreeRenderer); this._treeRenderer = this.instantiationService.createInstance(TimelineTreeRenderer, this._menus);
this._tree = <WorkbenchObjectTree<TreeElement, FuzzyScore>>this.instantiationService.createInstance(WorkbenchObjectTree, 'TimelinePane', this._treeElement, new TimelineListVirtualDelegate(), [renderer], { this._tree = <WorkbenchObjectTree<TreeElement, FuzzyScore>>this.instantiationService.createInstance(WorkbenchObjectTree, 'TimelinePane',
this._treeElement, new TimelineListVirtualDelegate(), [this._treeRenderer], {
identityProvider: new TimelineIdentityProvider(), identityProvider: new TimelineIdentityProvider(),
keyboardNavigationLabelProvider: new TimelineKeyboardNavigationLabelProvider(), keyboardNavigationLabelProvider: new TimelineKeyboardNavigationLabelProvider(),
overrideStyles: { overrideStyles: {
listBackground: this.getBackgroundColor() listBackground: this.getBackgroundColor(),
} }
}); });
const customTreeNavigator = new TreeResourceNavigator(this._tree, { openOnFocus: false, openOnSelection: false }); const customTreeNavigator = new TreeResourceNavigator(this._tree, { openOnFocus: false, openOnSelection: false });
this._register(customTreeNavigator); this._register(customTreeNavigator);
this._register(this._tree.onContextMenu(e => this.onContextMenu(this._menus, e)));
this._register( this._register(
customTreeNavigator.onDidOpenResource(e => { customTreeNavigator.onDidOpenResource(e => {
if (!e.browserEvent) { if (!e.browserEvent) {
...@@ -316,36 +334,112 @@ export class TimelinePane extends ViewPane { ...@@ -316,36 +334,112 @@ export class TimelinePane extends ViewPane {
}) })
); );
} }
private onContextMenu(menus: TimelineMenus, treeEvent: ITreeContextMenuEvent<TreeElement | null>): void {
const item = treeEvent.element;
if (item === null) {
return;
}
const event: UIEvent = treeEvent.browserEvent;
event.preventDefault();
event.stopPropagation();
this._tree.setFocus([item]);
const actions = menus.getResourceContextActions(item);
if (!actions.length) {
return;
}
this.contextMenuService.showContextMenu({
getAnchor: () => treeEvent.anchor,
getActions: () => actions,
getActionViewItem: (action) => {
const keybinding = this.keybindingService.lookupKeybinding(action.id);
if (keybinding) {
return new ActionViewItem(action, action, { label: true, keybinding: keybinding.getLabel() });
}
return undefined;
},
onHide: (wasCancelled?: boolean) => {
if (wasCancelled) {
this._tree.domFocus();
}
},
getActionsContext: (): TimelineActionContext => ({ uri: this._uri, item: item }),
actionRunner: new TimelineActionRunner()
});
}
} }
export class TimelineElementTemplate { export class TimelineElementTemplate implements IDisposable {
static readonly id = 'TimelineElementTemplate'; static readonly id = 'TimelineElementTemplate';
readonly actionBar: ActionBar;
readonly icon: HTMLElement;
readonly iconLabel: IconLabel;
readonly timestamp: HTMLSpanElement;
constructor( constructor(
readonly container: HTMLElement, readonly container: HTMLElement,
readonly iconLabel: IconLabel, actionViewItemProvider: IActionViewItemProvider
readonly icon: HTMLElement ) {
) { } DOM.addClass(container, 'custom-view-tree-node-item');
this.icon = DOM.append(container, DOM.$('.custom-view-tree-node-item-icon'));
this.iconLabel = new IconLabel(container, { supportHighlights: true, supportCodicons: true });
const timestampContainer = DOM.append(this.iconLabel.element, DOM.$('.timeline-timestamp-container'));
this.timestamp = DOM.append(timestampContainer, DOM.$('span.timeline-timestamp'));
const actionsContainer = DOM.append(this.iconLabel.element, DOM.$('.actions'));
this.actionBar = new ActionBar(actionsContainer, { actionViewItemProvider: actionViewItemProvider });
}
dispose() {
this.iconLabel.dispose();
this.actionBar.dispose();
}
reset() {
this.actionBar.clear();
}
}
export class TimelineIdentityProvider implements IIdentityProvider<TreeElement> {
getId(item: TreeElement): { toString(): string } {
return item.handle;
}
} }
export class TimelineIdentityProvider implements IIdentityProvider<TimelineItem> { class TimelineActionRunner extends ActionRunner {
getId(item: TimelineItem): { toString(): string } {
return `${item.id}|${item.timestamp}`; runAction(action: IAction, { uri, item }: TimelineActionContext): Promise<any> {
return action.run(...[
{
$mid: 11,
handle: item.handle,
source: item.source,
uri: uri
},
uri,
item.source,
]);
} }
} }
export class TimelineKeyboardNavigationLabelProvider implements IKeyboardNavigationLabelProvider<TimelineItem> { export class TimelineKeyboardNavigationLabelProvider implements IKeyboardNavigationLabelProvider<TreeElement> {
getKeyboardNavigationLabel(element: TimelineItem): { toString(): string } { getKeyboardNavigationLabel(element: TreeElement): { toString(): string } {
return element.label; return element.label;
} }
} }
export class TimelineListVirtualDelegate implements IListVirtualDelegate<TimelineItem> { export class TimelineListVirtualDelegate implements IListVirtualDelegate<TreeElement> {
getHeight(_element: TimelineItem): number { getHeight(_element: TreeElement): number {
return 22; return 22;
} }
getTemplateId(element: TimelineItem): string { getTemplateId(element: TreeElement): string {
return TimelineElementTemplate.id; return TimelineElementTemplate.id;
} }
} }
...@@ -353,14 +447,25 @@ export class TimelineListVirtualDelegate implements IListVirtualDelegate<Timelin ...@@ -353,14 +447,25 @@ export class TimelineListVirtualDelegate implements IListVirtualDelegate<Timelin
class TimelineTreeRenderer implements ITreeRenderer<TreeElement, FuzzyScore, TimelineElementTemplate> { class TimelineTreeRenderer implements ITreeRenderer<TreeElement, FuzzyScore, TimelineElementTemplate> {
readonly templateId: string = TimelineElementTemplate.id; readonly templateId: string = TimelineElementTemplate.id;
constructor(@IThemeService private _themeService: IThemeService) { } private _actionViewItemProvider: IActionViewItemProvider;
renderTemplate(container: HTMLElement): TimelineElementTemplate { constructor(
DOM.addClass(container, 'custom-view-tree-node-item'); private readonly _menus: TimelineMenus,
const icon = DOM.append(container, DOM.$('.custom-view-tree-node-item-icon')); @IInstantiationService protected readonly instantiationService: IInstantiationService,
@IThemeService private _themeService: IThemeService
) {
this._actionViewItemProvider = (action: IAction) => action instanceof MenuItemAction
? this.instantiationService.createInstance(ContextAwareMenuEntryActionViewItem, action)
: undefined;
}
private _uri: URI | undefined;
setUri(uri: URI | undefined) {
this._uri = uri;
}
const iconLabel = new IconLabel(container, { supportHighlights: true, supportCodicons: true }); renderTemplate(container: HTMLElement): TimelineElementTemplate {
return new TimelineElementTemplate(container, iconLabel, icon); return new TimelineElementTemplate(container, this._actionViewItemProvider);
} }
renderElement( renderElement(
...@@ -369,30 +474,74 @@ class TimelineTreeRenderer implements ITreeRenderer<TreeElement, FuzzyScore, Tim ...@@ -369,30 +474,74 @@ class TimelineTreeRenderer implements ITreeRenderer<TreeElement, FuzzyScore, Tim
template: TimelineElementTemplate, template: TimelineElementTemplate,
height: number | undefined height: number | undefined
): void { ): void {
const { element } = node; template.reset();
const icon = this._themeService.getTheme().type === LIGHT ? element.icon : element.iconDark; const { element: item } = node;
const icon = this._themeService.getTheme().type === LIGHT ? item.icon : item.iconDark;
const iconUrl = icon ? URI.revive(icon) : null; const iconUrl = icon ? URI.revive(icon) : null;
if (iconUrl) { if (iconUrl) {
template.icon.className = 'custom-view-tree-node-item-icon'; template.icon.className = 'custom-view-tree-node-item-icon';
template.icon.style.backgroundImage = DOM.asCSSUrl(iconUrl); template.icon.style.backgroundImage = DOM.asCSSUrl(iconUrl);
} else { } else {
let iconClass: string | undefined; let iconClass: string | undefined;
if (element.themeIcon /*&& !this.isFileKindThemeIcon(element.themeIcon)*/) { if (item.themeIcon /*&& !this.isFileKindThemeIcon(element.themeIcon)*/) {
iconClass = ThemeIcon.asClassName(element.themeIcon); iconClass = ThemeIcon.asClassName(item.themeIcon);
} }
template.icon.className = iconClass ? `custom-view-tree-node-item-icon ${iconClass}` : ''; template.icon.className = iconClass ? `custom-view-tree-node-item-icon ${iconClass}` : '';
} }
template.iconLabel.setLabel(element.label, element.description, { template.iconLabel.setLabel(item.label, item.description, {
title: element.detail, title: item.detail,
matches: createMatches(node.filterData) matches: createMatches(node.filterData)
}); });
template.timestamp.textContent = fromNow(item.timestamp);
template.actionBar.context = { uri: this._uri, item: item } as TimelineActionContext;
template.actionBar.actionRunner = new TimelineActionRunner();
template.actionBar.push(this._menus.getResourceActions(item), { icon: true, label: false });
} }
disposeTemplate(template: TimelineElementTemplate): void { disposeTemplate(template: TimelineElementTemplate): void {
template.iconLabel.dispose(); template.iconLabel.dispose();
} }
} }
class TimelineMenus extends Disposable {
constructor(
private id: string,
@IContextKeyService private readonly contextKeyService: IContextKeyService,
@IMenuService private readonly menuService: IMenuService,
@IContextMenuService private readonly contextMenuService: IContextMenuService
) {
super();
}
getResourceActions(element: TreeElement): IAction[] {
return this.getActions(MenuId.TimelineItemContext, { key: 'timelineItem', value: element.contextValue }).primary;
}
getResourceContextActions(element: TreeElement): IAction[] {
return this.getActions(MenuId.TimelineItemContext, { key: 'timelineItem', value: element.contextValue }).secondary;
}
private getActions(menuId: MenuId, context: { key: string, value?: string }): { primary: IAction[]; secondary: IAction[]; } {
const contextKeyService = this.contextKeyService.createScoped();
contextKeyService.createKey('view', this.id);
contextKeyService.createKey(context.key, context.value);
const menu = this.menuService.createMenu(menuId, contextKeyService);
const primary: IAction[] = [];
const secondary: IAction[] = [];
const result = { primary, secondary };
createAndFillInContextMenuActions(menu, { shouldForwardArgs: true }, result, this.contextMenuService, g => /^inline/.test(g));
menu.dispose();
contextKeyService.dispose();
return result;
}
}
...@@ -16,9 +16,11 @@ export function toKey(extension: ExtensionIdentifier | string, source: string) { ...@@ -16,9 +16,11 @@ export function toKey(extension: ExtensionIdentifier | string, source: string) {
} }
export interface TimelineItem { export interface TimelineItem {
handle: string;
source: string;
timestamp: number; timestamp: number;
label: string; label: string;
id?: string;
icon?: URI, icon?: URI,
iconDark?: URI, iconDark?: URI,
themeIcon?: { id: string }, themeIcon?: { id: string },
...@@ -28,10 +30,6 @@ export interface TimelineItem { ...@@ -28,10 +30,6 @@ export interface TimelineItem {
contextValue?: string; contextValue?: string;
} }
export interface TimelineItemWithSource extends TimelineItem {
source: string;
}
export interface TimelineChangeEvent { export interface TimelineChangeEvent {
id: string; id: string;
uri?: URI; uri?: URI;
...@@ -45,7 +43,7 @@ export interface TimelineCursor { ...@@ -45,7 +43,7 @@ export interface TimelineCursor {
export interface Timeline { export interface Timeline {
source: string; source: string;
items: TimelineItemWithSource[]; items: TimelineItem[];
cursor?: any; cursor?: any;
more?: boolean; more?: boolean;
...@@ -54,7 +52,7 @@ export interface Timeline { ...@@ -54,7 +52,7 @@ export interface Timeline {
export interface TimelineProvider extends TimelineProviderDescriptor, IDisposable { export interface TimelineProvider extends TimelineProviderDescriptor, IDisposable {
onDidChange?: Event<TimelineChangeEvent>; onDidChange?: Event<TimelineChangeEvent>;
provideTimeline(uri: URI, cursor: TimelineCursor, token: CancellationToken): Promise<Timeline | undefined>; provideTimeline(uri: URI, cursor: TimelineCursor, token: CancellationToken, options?: { cacheResults?: boolean }): Promise<Timeline | undefined>;
} }
export interface TimelineProviderDescriptor { export interface TimelineProviderDescriptor {
...@@ -86,7 +84,7 @@ export interface ITimelineService { ...@@ -86,7 +84,7 @@ export interface ITimelineService {
getSources(): string[]; getSources(): string[];
getTimeline(id: string, uri: URI, pagination: TimelineCursor, tokenSource: CancellationTokenSource): TimelineRequest | undefined; getTimeline(id: string, uri: URI, cursor: TimelineCursor, tokenSource: CancellationTokenSource, options?: { cacheResults?: boolean }): TimelineRequest | undefined;
} }
const TIMELINE_SERVICE_ID = 'timeline'; const TIMELINE_SERVICE_ID = 'timeline';
......
...@@ -81,7 +81,7 @@ export class TimelineService implements ITimelineService { ...@@ -81,7 +81,7 @@ export class TimelineService implements ITimelineService {
return [...this._providers.keys()]; return [...this._providers.keys()];
} }
getTimeline(id: string, uri: URI, cursor: TimelineCursor, tokenSource: CancellationTokenSource) { getTimeline(id: string, uri: URI, cursor: TimelineCursor, tokenSource: CancellationTokenSource, options?: { cacheResults?: boolean }) {
this.logService.trace(`TimelineService#getTimeline(${id}): uri=${uri.toString(true)}`); this.logService.trace(`TimelineService#getTimeline(${id}): uri=${uri.toString(true)}`);
const provider = this._providers.get(id); const provider = this._providers.get(id);
...@@ -98,7 +98,7 @@ export class TimelineService implements ITimelineService { ...@@ -98,7 +98,7 @@ export class TimelineService implements ITimelineService {
} }
return { return {
result: provider.provideTimeline(uri, cursor, tokenSource.token) result: provider.provideTimeline(uri, cursor, tokenSource.token, options)
.then(result => { .then(result => {
if (result === undefined) { if (result === undefined) {
return undefined; return undefined;
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册