diff --git a/.eslintrc.json b/.eslintrc.json index 6f9f7e338f7..2e15d11add0 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -568,7 +568,9 @@ "vscode-oniguruma", "iconv-lite-umd", "tas-client-umd", - "jschardet" + "jschardet", + "dayjs", + "dayjs/plugin/*" ] }, { @@ -600,6 +602,8 @@ "vscode-oniguruma", "iconv-lite-umd", "jschardet", + "dayjs", + "dayjs/plugin/*", "@vscode/vscode-languagedetection", "@microsoft/applicationinsights-web" ] @@ -633,7 +637,9 @@ "vscode-textmate", "vscode-oniguruma", "iconv-lite-umd", - "jschardet" + "jschardet", + "dayjs", + "dayjs/plugin/*" ] }, { @@ -753,7 +759,9 @@ "vscode-textmate", "vscode-oniguruma", "iconv-lite-umd", - "jschardet" + "jschardet", + "dayjs", + "dayjs/plugin/*" ] }, { @@ -788,7 +796,9 @@ "vscode-textmate", "vscode-oniguruma", "iconv-lite-umd", - "jschardet" + "jschardet", + "dayjs", + "dayjs/plugin/*" ] }, { diff --git a/package.json b/package.json index 72bf114bc6a..a3a49e0fb35 100644 --- a/package.json +++ b/package.json @@ -66,6 +66,7 @@ "@vscode/sudo-prompt": "9.3.1", "@vscode/vscode-languagedetection": "1.0.21", "applicationinsights": "1.4.2", + "dayjs": "^1.10.7", "graceful-fs": "4.2.8", "http-proxy-agent": "^2.1.0", "https-proxy-agent": "^2.2.3", diff --git a/remote/package.json b/remote/package.json index 1b8ad7101bb..a53eae5043f 100644 --- a/remote/package.json +++ b/remote/package.json @@ -9,6 +9,7 @@ "@vscode/vscode-languagedetection": "1.0.21", "applicationinsights": "1.4.2", "cookie": "^0.4.0", + "dayjs": "^1.10.7", "graceful-fs": "4.2.8", "http-proxy-agent": "^2.1.0", "https-proxy-agent": "^2.2.3", diff --git a/remote/yarn.lock b/remote/yarn.lock index 29df89a48c6..6556f726c00 100644 --- a/remote/yarn.lock +++ b/remote/yarn.lock @@ -204,6 +204,11 @@ data-uri-to-buffer@3: resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-3.0.1.tgz#594b8973938c5bc2c33046535785341abc4f3636" integrity sha512-WboRycPNsVw3B3TL559F7kuBUM4d8CgMEvk6xEJlOp7OBPjt6G7z8WMWlD2rOFZLk6OYfFIUGsCOWzcQH9K2og== +dayjs@^1.10.7: + version "1.10.7" + resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.10.7.tgz#2cf5f91add28116748440866a0a1d26f3a6ce468" + integrity sha512-P6twpd70BcPK34K26uJ1KT3wlhpuOAPoMwJzpsIWUxHZ7wpmbdZL/hQqBDfz7hGurYSa5PhzdhDHtt319hL3ig== + debug@3.1.0, debug@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" diff --git a/src/bootstrap-window.js b/src/bootstrap-window.js index 08c487263d6..9967c935431 100644 --- a/src/bootstrap-window.js +++ b/src/bootstrap-window.js @@ -142,7 +142,11 @@ 'iconv-lite-umd': `${baseNodeModulesPath}/iconv-lite-umd/lib/iconv-lite-umd.js`, 'jschardet': `${baseNodeModulesPath}/jschardet/dist/jschardet.min.js`, '@vscode/vscode-languagedetection': `${baseNodeModulesPath}/@vscode/vscode-languagedetection/dist/lib/index.js`, - 'tas-client-umd': `${baseNodeModulesPath}/tas-client-umd/lib/tas-client-umd.js` + 'tas-client-umd': `${baseNodeModulesPath}/tas-client-umd/lib/tas-client-umd.js`, + 'dayjs': `${baseNodeModulesPath}/dayjs/dayjs.min.js`, + 'dayjs/plugin/relativeTime': `${baseNodeModulesPath}/dayjs/plugin/relativeTime.js`, + 'dayjs/plugin/updateLocale': `${baseNodeModulesPath}/dayjs/plugin/updateLocale.js`, + 'dayjs/plugin/localizedFormat': `${baseNodeModulesPath}/dayjs/plugin/localizedFormat.js` }; // Allow to load built-in and other node.js modules via AMD diff --git a/src/vs/base/common/marshalling.ts b/src/vs/base/common/marshalling.ts index 789cdf08a16..e8edabcea5d 100644 --- a/src/vs/base/common/marshalling.ts +++ b/src/vs/base/common/marshalling.ts @@ -31,6 +31,7 @@ export const enum MarshalledId { TimelineActionContext, NotebookCellActionContext, TestItemContext, + Date, } export interface MarshalledObject { @@ -68,6 +69,7 @@ export function revive(obj: any, depth = 0): Revived { switch ((obj).$mid) { case MarshalledId.Uri: return URI.revive(obj); case MarshalledId.Regexp: return new RegExp(obj.source, obj.flags); + case MarshalledId.Date: return new Date(obj.source); } if ( diff --git a/src/vs/editor/common/languages.ts b/src/vs/editor/common/languages.ts index f05132706eb..52c84e3e2e7 100644 --- a/src/vs/editor/common/languages.ts +++ b/src/vs/editor/common/languages.ts @@ -1716,6 +1716,7 @@ export interface Comment { readonly commentReactions?: CommentReaction[]; readonly label?: string; readonly mode?: CommentMode; + readonly detail?: Date | string; } /** diff --git a/src/vs/workbench/api/browser/mainThreadComments.ts b/src/vs/workbench/api/browser/mainThreadComments.ts index 6c0a4b1c4c3..d85214116b9 100644 --- a/src/vs/workbench/api/browser/mainThreadComments.ts +++ b/src/vs/workbench/api/browser/mainThreadComments.ts @@ -15,7 +15,7 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; import { ICommentInfo, ICommentService } from 'vs/workbench/contrib/comments/browser/commentService'; import { CommentsPanel } from 'vs/workbench/contrib/comments/browser/commentsView'; -import { CommentProviderFeatures, ExtHostCommentsShape, ExtHostContext, IExtHostContext, MainContext, MainThreadCommentsShape, CommentThreadChanges } from '../common/extHost.protocol'; +import { CommentProviderFeatures, ExtHostCommentsShape, ExtHostContext, IExtHostContext, MainContext, MainThreadCommentsShape, CommentThreadChanges, CommentChanges } from '../common/extHost.protocol'; import { COMMENTS_VIEW_ID, COMMENTS_VIEW_TITLE } from 'vs/workbench/contrib/comments/browser/commentsTreeViewer'; import { ViewContainer, IViewContainersRegistry, Extensions as ViewExtensions, ViewContainerLocation, IViewsRegistry, IViewsService, IViewDescriptorService } from 'vs/workbench/common/views'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; @@ -23,7 +23,7 @@ import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneCont import { Codicon } from 'vs/base/common/codicons'; import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; import { localize } from 'vs/nls'; -import { MarshalledId } from 'vs/base/common/marshalling'; +import { MarshalledId, revive } from 'vs/base/common/marshalling'; export class MainThreadCommentThread implements modes.CommentThread { @@ -139,11 +139,27 @@ export class MainThreadCommentThread implements modes.CommentThread { if (modified('range')) { this._range = changes.range!; } if (modified('label')) { this._label = changes.label; } if (modified('contextValue')) { this._contextValue = changes.contextValue === null ? undefined : changes.contextValue; } - if (modified('comments')) { this._comments = changes.comments; } + if (modified('comments')) { this._comments = this.commentsFromCommentChanges(changes.comments); } if (modified('collapseState')) { this._collapsibleState = changes.collapseState; } if (modified('canReply')) { this.canReply = changes.canReply!; } } + private commentsFromCommentChanges(comments?: CommentChanges[]): modes.Comment[] | undefined { + return comments?.map(comment => { + return { + body: comment.body, + uniqueIdInThread: comment.uniqueIdInThread, + userName: comment.userName, + commentReactions: comment.commentReactions, + contextValue: comment.contextValue, + detail: comment.detail ? revive(comment.detail) : undefined, + label: comment.label, + mode: comment.mode, + userIconPath: comment.userIconPath + }; + }); + } + dispose() { this._isDisposed = true; this._onDidChangeCollasibleState.dispose(); diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 6dfeca21e4f..a6db1a899f8 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -10,7 +10,7 @@ import { SerializedError } from 'vs/base/common/errors'; import { IRelativePattern } from 'vs/base/common/glob'; import { IMarkdownString } from 'vs/base/common/htmlContent'; import { IDisposable } from 'vs/base/common/lifecycle'; -import { revive } from 'vs/base/common/marshalling'; +import { MarshalledId, revive } from 'vs/base/common/marshalling'; import * as performance from 'vs/base/common/performance'; import Severity from 'vs/base/common/severity'; import { Dto } from 'vs/base/common/types'; @@ -160,11 +160,25 @@ export interface CommentProviderFeatures { options?: modes.CommentOptions; } +export interface CommentChanges { + readonly uniqueIdInThread: number; + readonly body: IMarkdownString; + readonly userName: string; + readonly userIconPath?: string; + readonly contextValue?: string; + readonly commentReactions?: modes.CommentReaction[]; + readonly label?: string; + readonly mode?: modes.CommentMode; + readonly detail?: { + $mid: MarshalledId.Date + } | string; +} + export type CommentThreadChanges = Partial<{ range: IRange, label: string, contextValue: string | null, - comments: modes.Comment[], + comments: CommentChanges[], collapseState: modes.CommentThreadCollapsibleState; canReply: boolean; }>; diff --git a/src/vs/workbench/api/common/extHostComments.ts b/src/vs/workbench/api/common/extHostComments.ts index 190c0f44d39..915d28eb866 100644 --- a/src/vs/workbench/api/common/extHostComments.ts +++ b/src/vs/workbench/api/common/extHostComments.ts @@ -16,8 +16,9 @@ import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensio import { ExtHostDocuments } from 'vs/workbench/api/common/extHostDocuments'; import * as extHostTypeConverter from 'vs/workbench/api/common/extHostTypeConverters'; import * as types from 'vs/workbench/api/common/extHostTypes'; +import { checkProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; import type * as vscode from 'vscode'; -import { ExtHostCommentsShape, IMainContext, MainContext, CommentThreadChanges } from './extHost.protocol'; +import { ExtHostCommentsShape, IMainContext, MainContext, CommentThreadChanges, CommentChanges } from './extHost.protocol'; import { ExtHostCommands } from './extHostCommands'; type ProviderHandle = number; @@ -346,7 +347,7 @@ export function createExtHostComments(mainContext: IMainContext, commands: ExtHo private _uri: vscode.Uri, private _range: vscode.Range, private _comments: vscode.Comment[], - extensionId: ExtensionIdentifier + public readonly extensionDescription: IExtensionDescription ) { this._acceptInputDisposables.value = new DisposableStore(); @@ -360,7 +361,7 @@ export function createExtHostComments(mainContext: IMainContext, commands: ExtHo this._id, this._uri, extHostTypeConverter.Range.from(this._range), - extensionId + extensionDescription.identifier ); this._localDisposables = []; @@ -433,7 +434,7 @@ export function createExtHostComments(mainContext: IMainContext, commands: ExtHo } if (modified('comments')) { formattedModifications.comments = - this._comments.map(cmt => convertToModeComment(this, cmt, this._commentsMap)); + this._comments.map(cmt => convertToDTOComment(this, cmt, this._commentsMap)); } if (modified('collapsibleState')) { formattedModifications.collapseState = convertToCollapsibleState(this._collapseState); @@ -561,18 +562,18 @@ export function createExtHostComments(mainContext: IMainContext, commands: ExtHo createCommentThread(resource: vscode.Uri, range: vscode.Range, comments: vscode.Comment[]): ExtHostCommentThread; createCommentThread(arg0: vscode.Uri | string, arg1: vscode.Uri | vscode.Range, arg2: vscode.Range | vscode.Comment[], arg3?: vscode.Comment[]): vscode.CommentThread { if (typeof arg0 === 'string') { - const commentThread = new ExtHostCommentThread(this.id, this.handle, arg0, arg1 as vscode.Uri, arg2 as vscode.Range, arg3 as vscode.Comment[], this._extension.identifier); + const commentThread = new ExtHostCommentThread(this.id, this.handle, arg0, arg1 as vscode.Uri, arg2 as vscode.Range, arg3 as vscode.Comment[], this._extension); this._threads.set(commentThread.handle, commentThread); return commentThread; } else { - const commentThread = new ExtHostCommentThread(this.id, this.handle, undefined, arg0 as vscode.Uri, arg1 as vscode.Range, arg2 as vscode.Comment[], this._extension.identifier); + const commentThread = new ExtHostCommentThread(this.id, this.handle, undefined, arg0 as vscode.Uri, arg1 as vscode.Range, arg2 as vscode.Comment[], this._extension); this._threads.set(commentThread.handle, commentThread); return commentThread; } } $createCommentThreadTemplate(uriComponents: UriComponents, range: IRange): ExtHostCommentThread { - const commentThread = new ExtHostCommentThread(this.id, this.handle, undefined, URI.revive(uriComponents), extHostTypeConverter.Range.to(range), [], this._extension.identifier); + const commentThread = new ExtHostCommentThread(this.id, this.handle, undefined, URI.revive(uriComponents), extHostTypeConverter.Range.to(range), [], this._extension); commentThread.collapsibleState = modes.CommentThreadCollapsibleState.Expanded; this._threads.set(commentThread.handle, commentThread); return commentThread; @@ -608,7 +609,7 @@ export function createExtHostComments(mainContext: IMainContext, commands: ExtHo } } - function convertToModeComment(thread: ExtHostCommentThread, vscodeComment: vscode.Comment, commentsMap: Map): modes.Comment { + function convertToDTOComment(thread: ExtHostCommentThread, vscodeComment: vscode.Comment, commentsMap: Map): CommentChanges { let commentUniqueId = commentsMap.get(vscodeComment)!; if (!commentUniqueId) { commentUniqueId = ++thread.commentHandle; @@ -617,6 +618,20 @@ export function createExtHostComments(mainContext: IMainContext, commands: ExtHo const iconPath = vscodeComment.author && vscodeComment.author.iconPath ? vscodeComment.author.iconPath.toString() : undefined; + if (vscodeComment.detail) { + checkProposedApiEnabled(thread.extensionDescription, 'commentTimestamp'); + } + + let detail: { $mid: MarshalledId.Date, source: any } | string | undefined; + if (vscodeComment.detail && (typeof vscodeComment.detail !== 'string')) { + detail = { + source: vscodeComment.detail, + $mid: MarshalledId.Date + }; + } else { + detail = vscodeComment.detail; + } + return { mode: vscodeComment.mode, contextValue: vscodeComment.contextValue, @@ -625,7 +640,8 @@ export function createExtHostComments(mainContext: IMainContext, commands: ExtHo userName: vscodeComment.author.name, userIconPath: iconPath, label: vscodeComment.label, - commentReactions: vscodeComment.reactions ? vscodeComment.reactions.map(reaction => convertToReaction(reaction)) : undefined + commentReactions: vscodeComment.reactions ? vscodeComment.reactions.map(reaction => convertToReaction(reaction)) : undefined, + detail: detail }; } @@ -661,3 +677,4 @@ export function createExtHostComments(mainContext: IMainContext, commands: ExtHo return new ExtHostCommentsImpl(); } + diff --git a/src/vs/workbench/contrib/comments/browser/commentNode.ts b/src/vs/workbench/contrib/comments/browser/commentNode.ts index 51763c76158..bae1f3ed23f 100644 --- a/src/vs/workbench/contrib/comments/browser/commentNode.ts +++ b/src/vs/workbench/contrib/comments/browser/commentNode.ts @@ -36,6 +36,8 @@ import { ActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; import { DropdownMenuActionViewItem } from 'vs/base/browser/ui/dropdown/dropdownActionViewItem'; import { Codicon } from 'vs/base/common/codicons'; import { MarshalledId } from 'vs/base/common/marshalling'; +import { TimestampWidget } from 'vs/workbench/contrib/comments/browser/timestamp'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; export class CommentNode extends Disposable { private _domNode: HTMLElement; @@ -53,6 +55,8 @@ export class CommentNode extends Disposable { private _commentEditorDisposables: IDisposable[] = []; private _commentEditorModel: ITextModel | null = null; private _isPendingLabel!: HTMLElement; + private _detail: HTMLElement | undefined; + private _timestamp: TimestampWidget | undefined; private _contextKeyService: IContextKeyService; private _commentContextValue: IContextKey; @@ -83,7 +87,8 @@ export class CommentNode extends Disposable { @ILanguageService private languageService: ILanguageService, @INotificationService private notificationService: INotificationService, @IContextMenuService private contextMenuService: IContextMenuService, - @IContextKeyService contextKeyService: IContextKeyService + @IContextKeyService contextKeyService: IContextKeyService, + @IConfigurationService private configurationService: IConfigurationService ) { super(); @@ -121,11 +126,38 @@ export class CommentNode extends Disposable { return this._onDidClick.event; } + private createDetail(container: HTMLElement) { + this._detail = dom.append(container, dom.$('span.detail')); + this.updateDetail(this.comment.detail); + } + + private updateDetail(detail?: Date | string) { + if (!this._detail) { + return; + } + + if (!detail) { + this._timestamp?.dispose(); + this._detail.innerText = ''; + } else if (typeof detail === 'string') { + this._timestamp?.dispose(); + this._detail.innerText = detail; + } else { + this._detail.innerText = ''; + if (!this._timestamp) { + this._timestamp = new TimestampWidget(this.configurationService, this._detail, detail); + this._register(this._timestamp); + } else { + this._timestamp.setTimestamp(detail); + } + } + } + private createHeader(commentDetailsContainer: HTMLElement): void { const header = dom.append(commentDetailsContainer, dom.$(`div.comment-title.${MOUSE_CURSOR_TEXT_CSS_CLASS_NAME}`)); const author = dom.append(header, dom.$('strong.author')); author.innerText = this.comment.userName; - + this.createDetail(header); this._isPendingLabel = dom.append(header, dom.$('span.isPending')); if (this.comment.label) { @@ -516,6 +548,10 @@ export class CommentNode extends Disposable { } else { this._commentContextValue.reset(); } + + if (this.comment.detail) { + this.updateDetail(this.comment.detail); + } } focus() { diff --git a/src/vs/workbench/contrib/comments/browser/comments.contribution.ts b/src/vs/workbench/contrib/comments/browser/comments.contribution.ts index d01db82f2c1..f2c6b1d3b4b 100644 --- a/src/vs/workbench/contrib/comments/browser/comments.contribution.ts +++ b/src/vs/workbench/contrib/comments/browser/comments.contribution.ts @@ -25,6 +25,12 @@ Registry.as(ConfigurationExtensions.Configuration).regis default: 'openOnSessionStartWithComments', description: nls.localize('openComments', "Controls when the comments panel should open."), restricted: false + }, + 'comments.useRelativeTime': { + type: 'boolean', + default: true, + description: nls.localize('useRelativeTime', "Determines if relative time will be used in comment timestamps (ex. '1 day ago').") + } } }); diff --git a/src/vs/workbench/contrib/comments/browser/media/review.css b/src/vs/workbench/contrib/comments/browser/media/review.css index 0791ae5b94d..8bf070482b7 100644 --- a/src/vs/workbench/contrib/comments/browser/media/review.css +++ b/src/vs/workbench/contrib/comments/browser/media/review.css @@ -102,6 +102,12 @@ font-style: italic; } +.monaco-editor .review-widget .body .review-comment .review-comment-contents .timestamp { + line-height: 22px; + margin: 0 5px 0 5px; + padding: 0 2px 0 2px; +} + .monaco-editor .review-widget .body .review-comment .review-comment-contents .comment-body { padding-top: 4px; } diff --git a/src/vs/workbench/contrib/comments/browser/timestamp.ts b/src/vs/workbench/contrib/comments/browser/timestamp.ts new file mode 100644 index 00000000000..bf6134838bd --- /dev/null +++ b/src/vs/workbench/contrib/comments/browser/timestamp.ts @@ -0,0 +1,115 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as dom from 'vs/base/browser/dom'; +import { Disposable } from 'vs/base/common/lifecycle'; +import * as dayjs from 'dayjs'; +import * as relativeTime from 'dayjs/plugin/relativeTime'; +import * as updateLocale from 'dayjs/plugin/updateLocale'; +import * as localizedFormat from 'dayjs/plugin/localizedFormat'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; + +const USE_RELATIVE_TIME_CONFIGURATION = 'comments.useRelativeTime'; + +dayjs.extend(relativeTime, { + thresholds: [ + { l: 's', r: 44, d: 'second' }, + { l: 'm', r: 89 }, + { l: 'mm', r: 44, d: 'minute' }, + { l: 'h', r: 89 }, + { l: 'hh', r: 21, d: 'hour' }, + { l: 'd', r: 35 }, + { l: 'dd', r: 6, d: 'day' }, + { l: 'w', r: 7 }, + { l: 'ww', r: 3, d: 'week' }, + { l: 'M', r: 4 }, + { l: 'MM', r: 10, d: 'month' }, + { l: 'y', r: 17 }, + { l: 'yy', d: 'year' }, + ], +}); + +dayjs.extend(updateLocale); +dayjs.updateLocale('en', { + relativeTime: { + past: '%s ago', + s: 'seconds', + m: 'a minute', + mm: '%d minutes', + h: 'an hour', + hh: '%d hours', + d: 'a day', + dd: '%d days', + w: 'a week', + ww: '%d weeks', + M: 'a month', + MM: '%d months', + y: 'a year', + yy: '%d years', + }, +}); +dayjs.extend(localizedFormat); + +export class TimestampWidget extends Disposable { + private _date: HTMLElement; + private _timestamp: Date | undefined; + private _useRelativeTime: boolean; + + constructor(private configurationService: IConfigurationService, container: HTMLElement, timeStamp?: Date) { + super(); + this._date = dom.append(container, dom.$('span.timestamp')); + this._useRelativeTime = this.useRelativeTimeSetting; + this.setTimestamp(timeStamp); + } + + private get useRelativeTimeSetting(): boolean { + return this.configurationService.getValue(USE_RELATIVE_TIME_CONFIGURATION); + } + + public async setTimestamp(timestamp: Date | undefined) { + if ((timestamp !== this._timestamp) || (this.useRelativeTimeSetting !== this._useRelativeTime)) { + this.updateDate(timestamp); + } + this._timestamp = timestamp; + this._useRelativeTime = this.useRelativeTimeSetting; + } + + private updateDate(timestamp?: Date) { + if (!timestamp) { + this._date.textContent = ''; + } else if ((timestamp !== this._timestamp) + || (this.useRelativeTimeSetting !== this._useRelativeTime)) { + + let textContent: string; + let tooltip: string | undefined; + if (this.useRelativeTimeSetting) { + textContent = this.getRelative(timestamp); + tooltip = this.getDateString(timestamp); + } else { + textContent = this.getDateString(timestamp); + } + + this._date.textContent = textContent; + if (tooltip) { + this._date.title = tooltip; + } + } + } + + private getRelative(date: Date): string { + const djs = dayjs(date); + const now = Date.now(); + const diff = djs.diff(now, 'month'); + if ((diff < 1) && (diff > -11)) { + return djs.fromNow(); + } + return this.getDateString(date); + } + + private getDateString(date: Date): string { + const djs = dayjs(date); + return djs.format('lll'); + } +} diff --git a/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts b/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts index 6e72752e6af..ca0a04329d4 100644 --- a/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts +++ b/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts @@ -7,6 +7,7 @@ export const allApiProposals = Object.freeze({ authSession: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.authSession.d.ts', + commentTimestamp: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.commentTimestamp.d.ts', contribIconFonts: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribIconFonts.d.ts', contribIcons: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribIcons.d.ts', contribLabelFormatterWorkspaceTooltip: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribLabelFormatterWorkspaceTooltip.d.ts', diff --git a/src/vscode-dts/vscode.proposed.commentTimestamp.d.ts b/src/vscode-dts/vscode.proposed.commentTimestamp.d.ts new file mode 100644 index 00000000000..1039db61b98 --- /dev/null +++ b/src/vscode-dts/vscode.proposed.commentTimestamp.d.ts @@ -0,0 +1,15 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare module 'vscode' { + export interface Comment { + /** + * An optional detail that will be displayed less prominently than the `author`. + * If a date is provided, then the date will be formatted according to the user's + * locale and settings. + */ + detail?: Date | string + } +} diff --git a/yarn.lock b/yarn.lock index 2fb414cb0dd..4f77d187173 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3071,6 +3071,11 @@ data-uri-to-buffer@3: resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-3.0.1.tgz#594b8973938c5bc2c33046535785341abc4f3636" integrity sha512-WboRycPNsVw3B3TL559F7kuBUM4d8CgMEvk6xEJlOp7OBPjt6G7z8WMWlD2rOFZLk6OYfFIUGsCOWzcQH9K2og== +dayjs@^1.10.7: + version "1.10.7" + resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.10.7.tgz#2cf5f91add28116748440866a0a1d26f3a6ce468" + integrity sha512-P6twpd70BcPK34K26uJ1KT3wlhpuOAPoMwJzpsIWUxHZ7wpmbdZL/hQqBDfz7hGurYSa5PhzdhDHtt319hL3ig== + debounce@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/debounce/-/debounce-1.1.0.tgz#6a1a4ee2a9dc4b7c24bb012558dbcdb05b37f408"