diff --git a/docs/API-Reference/utils/LocalizationUtils.md b/docs/API-Reference/utils/LocalizationUtils.md index 2b9cb0035d..efe3950c23 100644 --- a/docs/API-Reference/utils/LocalizationUtils.md +++ b/docs/API-Reference/utils/LocalizationUtils.md @@ -34,7 +34,7 @@ Formats a given date object into a locale-aware date and time string. -## dateTimeFromNow([date], [lang]) ⇒ string +## dateTimeFromNow([date], [lang], [fromDate]) ⇒ string Returns a relative time string (e.g., "2 days ago", "in 3 hours") based on the difference between the given date and now. **Kind**: global function @@ -44,6 +44,7 @@ Returns a relative time string (e.g., "2 days ago", "in 3 hours") based on the d | --- | --- | --- | | [date] | Date | The date to compare with the current date and time. If not given, defaults to now. | | [lang] | string | Optional language code to use for formatting (e.g., 'en', 'fr'). If not provided, defaults to the application locale or 'en'. | +| [fromDate] | string | Optional date to use instead of now to compute the relative dateTime from. | diff --git a/src/document/DocumentCommandHandlers.js b/src/document/DocumentCommandHandlers.js index afd026f374..525659da69 100644 --- a/src/document/DocumentCommandHandlers.js +++ b/src/document/DocumentCommandHandlers.js @@ -1957,6 +1957,9 @@ define(function (require, exports, module) { if (entry) { brackets.app.openPathInFileBrowser(entry.fullPath) .catch(err=>console.error("Error showing '" + entry.fullPath + "' in OS folder:", err)); + } else { + brackets.app.openPathInFileBrowser(ProjectManager.getProjectRoot().fullPath) + .catch(err=>console.error("Error showing '" + ProjectManager.getProjectRoot().fullPath + "' in OS folder:", err)); } } @@ -1964,6 +1967,8 @@ define(function (require, exports, module) { const entry = ProjectManager.getSelectedItem(); if (entry && entry.fullPath) { NodeUtils.openNativeTerminal(entry.fullPath); + } else { + NodeUtils.openNativeTerminal(ProjectManager.getProjectRoot().fullPath); } } @@ -1971,13 +1976,17 @@ define(function (require, exports, module) { const entry = ProjectManager.getSelectedItem(); if (entry && entry.fullPath) { NodeUtils.openNativeTerminal(entry.fullPath, true); + } else { + NodeUtils.openNativeTerminal(ProjectManager.getProjectRoot().fullPath, true); } } function openDefaultApp() { const entry = ProjectManager.getSelectedItem(); if (entry && entry.fullPath) { - NodeUtils.openInDefaultApp(entry.fullPath, true); + NodeUtils.openInDefaultApp(entry.fullPath); + } else { + NodeUtils.openInDefaultApp(ProjectManager.getProjectRoot().fullPath); } } diff --git a/src/styles/brackets_core_ui_variables.less b/src/styles/brackets_core_ui_variables.less index dd07a3c947..9266d8e41d 100644 --- a/src/styles/brackets_core_ui_variables.less +++ b/src/styles/brackets_core_ui_variables.less @@ -108,7 +108,7 @@ @bc-panel-bg-hover: rgba(255, 255, 255, 0.6); @bc-panel-bg-hover-alt: rgba(0, 0, 0, 0.04); @bc-panel-bg-selected: #d0d5d5; -@bc-panel-bg-selected-table: #b8c9d1; +@bc-panel-bg-selected-table: #cddae0; @bc-panel-bg-text-highlight: #fff; @bc-panel-border: rgba(0, 0, 0, 0.09); @bc-panel-separator: #c3c6c5; diff --git a/src/utils/LocalizationUtils.js b/src/utils/LocalizationUtils.js index 19118b204a..7abe394f24 100644 --- a/src/utils/LocalizationUtils.js +++ b/src/utils/LocalizationUtils.js @@ -77,28 +77,31 @@ define(function (require, exports, module) { * @param {Date} [date] - The date to compare with the current date and time. If not given, defaults to now. * @param {string} [lang] - Optional language code to use for formatting (e.g., 'en', 'fr'). * If not provided, defaults to the application locale or 'en'. + * @param {Date} [fromDate] - Optional date to use instead of now to compute the relative dateTime from. * @returns {string} - A human-readable relative time string (e.g., "2 days ago", "in 3 hours"). */ - function dateTimeFromNow(date, lang) { + function dateTimeFromNow(date, lang, fromDate) { date = date || new Date(); - const now = new Date(); - const diffInSeconds = Math.floor((date - now) / 1000); + fromDate = fromDate || new Date(); + const diffInSeconds = Math.floor((date - fromDate) / 1000); const rtf = new Intl.RelativeTimeFormat([lang || brackets.getLocale() || "en", "en"], { numeric: 'auto' }); - - if (Math.abs(diffInSeconds) < 60) { + if (Math.abs(diffInSeconds) < 3) { + // we consider diffs less than 3 seconds to be always 'now', for better UX and for unit tests stability. + return rtf.format(0, 'second'); + } else if (Math.abs(diffInSeconds) < 60) { return rtf.format(diffInSeconds, 'second'); } else if (Math.abs(diffInSeconds) < 3600) { - return rtf.format(Math.floor(diffInSeconds / 60), 'minute'); + return rtf.format(Math.trunc(diffInSeconds / 60), 'minute'); } else if (Math.abs(diffInSeconds) < 86400) { - return rtf.format(Math.floor(diffInSeconds / 3600), 'hour'); + return rtf.format(Math.trunc(diffInSeconds / 3600), 'hour'); } else if (Math.abs(diffInSeconds) < 2592000) { - return rtf.format(Math.floor(diffInSeconds / 86400), 'day'); + return rtf.format(Math.trunc(diffInSeconds / 86400), 'day'); } else if (Math.abs(diffInSeconds) < 31536000) { - return rtf.format(Math.floor(diffInSeconds / 2592000), 'month'); + return rtf.format(Math.trunc(diffInSeconds / 2592000), 'month'); } else { - return rtf.format(Math.floor(diffInSeconds / 31536000), 'year'); + return rtf.format(Math.trunc(diffInSeconds / 31536000), 'year'); } } @@ -115,7 +118,7 @@ define(function (require, exports, module) { function dateTimeFromNowFriendly(date, lang) { const now = new Date(); const diffInMilliseconds = date - now; - const diffInDays = Math.floor(diffInMilliseconds / (1000 * 60 * 60 * 24)); + const diffInDays = Math.trunc(diffInMilliseconds / (1000 * 60 * 60 * 24)); // If within the last 30 days or the future, use relative time if (Math.abs(diffInDays) <= 30) { diff --git a/test/spec/LocalizationUtils-test.js b/test/spec/LocalizationUtils-test.js index 34e2bf898a..e91099c9fb 100644 --- a/test/spec/LocalizationUtils-test.js +++ b/test/spec/LocalizationUtils-test.js @@ -132,64 +132,118 @@ define(function (require, exports, module) { }); describe("dateTimeFromNow", function () { - it("should return 'now' for current time", function () { - const now = new Date(); + // we take a reference date for tests stability. due to use of math.trunk, using date.now will + // sometimes lead to valid time jumps in tests. so we just take a reference time fixed to not account for + // any time taken in compute for tests. + const refDate = Date.now(); + function referenceDate({ + seconds = 0, + minutes = 0, + hours = 0, + days = 0, + months = 0, + years = 0 + } = {}) { + const date = new Date(refDate); // Original reference date + + // Add time components + date.setSeconds(date.getSeconds() + seconds); + date.setMinutes(date.getMinutes() + minutes); + date.setHours(date.getHours() + hours); + date.setDate(date.getDate() + days); + date.setMonth(date.getMonth() + months); + date.setFullYear(date.getFullYear() + years); + + return date; + } + + it("should return 'now' for current time without specifying fromTime", function () { + let now = new Date(); let result = LocalizationUtils.dateTimeFromNow(now, "en"); expect(result).toBe("now"); result = LocalizationUtils.dateTimeFromNow(now, "de"); expect(result).toBe("jetzt"); }); + it("should return 'now' for current time within 3 seconds of time", function () { + let now = referenceDate(); + let result = LocalizationUtils.dateTimeFromNow(now, "en", referenceDate()); + expect(result).toBe("now"); + now = referenceDate({seconds: 1}); + result = LocalizationUtils.dateTimeFromNow(now, "en", referenceDate()); + expect(result).toBe("now"); + now = referenceDate({seconds: 2}); + result = LocalizationUtils.dateTimeFromNow(now, "en", referenceDate()); + expect(result).toBe("now"); + result = LocalizationUtils.dateTimeFromNow(now, "de", referenceDate()); + expect(result).toBe("jetzt"); + now = referenceDate({seconds: 1}); + result = LocalizationUtils.dateTimeFromNow(now, "de", referenceDate()); + expect(result).toBe("jetzt"); + now = referenceDate({seconds: 2}); + result = LocalizationUtils.dateTimeFromNow(now, "de", referenceDate()); + expect(result).toBe("jetzt"); + }); + it("should handle future dates within seconds", function () { - const futureDate = new Date(Date.now() + 30 * 1000); // 30 seconds in the future - const result = LocalizationUtils.dateTimeFromNow(futureDate, "en"); + const futureDate = referenceDate({seconds: 30}); // 30 seconds in the future + const result = LocalizationUtils.dateTimeFromNow(futureDate, "en", referenceDate()); expect(result).toBe("in 30 seconds"); }); it("should handle past dates within minutes", function () { - const pastDate = new Date(Date.now() - 90 * 1000); // 90 seconds in the past - const result = LocalizationUtils.dateTimeFromNow(pastDate, "en"); + const pastDate = referenceDate({seconds: -90}); // 90 seconds in the past + const result = LocalizationUtils.dateTimeFromNow(pastDate, "en", referenceDate()); + expect(result).toBe("1 minute ago"); + }); + + it("should handle past and future dates similarly", function () { + const pastDate = referenceDate({seconds: -130}); // 2 minutes and 10 secs + let result = LocalizationUtils.dateTimeFromNow(pastDate, "en", referenceDate()); expect(result).toBe("2 minutes ago"); + const futureDate = referenceDate({seconds: 130}); // 2 minutes and 10 secs + result = LocalizationUtils.dateTimeFromNow(futureDate, "en", referenceDate()); + expect(result).toBe("in 2 minutes"); }); it("should handle future dates within hours", function () { - const futureDate = new Date(Date.now() + 2 * 60 * 60 * 1000); // 2 hours in the future - const result = LocalizationUtils.dateTimeFromNow(futureDate, "en"); + const futureDate = referenceDate({hours: 2}); // 2 hours in the future + const result = LocalizationUtils.dateTimeFromNow(futureDate, "en", referenceDate()); expect(result).toBe("in 2 hours"); }); it("should handle past dates within days", function () { - const pastDate = new Date(Date.now() - 3 * 24 * 60 * 60 * 1000); // 3 days ago - const result = LocalizationUtils.dateTimeFromNow(pastDate, "en"); + const pastDate = referenceDate({ days: -3 }); // 3 days ago + const result = LocalizationUtils.dateTimeFromNow(pastDate, "en", referenceDate()); expect(result).toBe("3 days ago"); }); it("should handle future dates within months", function () { - const futureDate = new Date(Date.now() + 45 * 24 * 60 * 60 * 1000); // 45 days in the future - const result = LocalizationUtils.dateTimeFromNow(futureDate, "en"); + const futureDate = referenceDate({ days: 45 }); // 45 days in the future + const result = LocalizationUtils.dateTimeFromNow(futureDate, "en", referenceDate()); expect(result).toBe("next month"); }); it("should handle past dates within years", function () { - const pastDate = new Date(Date.now() - 2 * 365 * 24 * 60 * 60 * 1000); // 2 years ago - const result = LocalizationUtils.dateTimeFromNow(pastDate, "en"); + const pastDate = referenceDate({ years: -2 }); // 2 years ago + const result = LocalizationUtils.dateTimeFromNow(pastDate, "en", referenceDate()); expect(result).toBe("2 years ago"); }); it("should return relative time in French locale", function () { - const pastDate = new Date(Date.now() - 3 * 24 * 60 * 60 * 1000); // 3 days ago - const result = LocalizationUtils.dateTimeFromNow(pastDate, "fr"); + const pastDate = referenceDate({ days: -3 }); // 3 days ago + const result = LocalizationUtils.dateTimeFromNow(pastDate, "fr", referenceDate()); expect(result).toBe("il y a 3 jours"); }); it("should fallback to default locale if an invalid locale is specified", function () { - const futureDate = new Date(Date.now() + 2 * 60 * 60 * 1000); // 2 hours in the future - const result = LocalizationUtils.dateTimeFromNow(futureDate, "invalid-locale"); + const futureDate = referenceDate({ hours: 2 }); // 2 hours in the future + const result = LocalizationUtils.dateTimeFromNow(futureDate, "invalid-locale", referenceDate()); expect(result).toBe("in 2 hours"); }); it("should handle default date input (now) gracefully", function () { - const result = LocalizationUtils.dateTimeFromNow(undefined, "en"); + const result = LocalizationUtils.dateTimeFromNow(); expect(result).toBe("now"); }); });