New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Recent list in dock does not show recent files/folders #74788
Comments
If someone wants to chime in, the code is in |
Hi. I am interested in working on this issue. I just want to make sure that I understood this correctly:
|
@bpasero Thanks for the information. However I couldn't seem to find the key associated to that setting. I have looked through domains |
@deepak1556 do you maybe have an idea where this setting could be found? |
Does this help? https://www.blackbagtech.com/blog/2017/01/17/recent-items-in-macos-sierra/ Not sure if this is accessible for third party applications, or if it's even the right value to be looking at, but the article mentions |
@connorshea unfortunately I do not seem to be able to get that value from that string even though it looks very promising... |
…74788 macOS only shows recent list with max number based on sytem config. It takes bottom n entries from the list added through Electron app.addRecentDocument. To match macOS dock recent list with VSCode internal recent list, this change add items into macOS recent document list in reverse order of internal recent list.
hi @bpasero , please review if my fix makes sense. macOS enforces the number of entries in recent list based on the config. When the number of entries in recent list is bigger than the config, macOS takes bottom n items. That's why the dock recent list and VSCode recent list seem not matching. In the change, I revert the items when adding into recent list through app.AddRecentDocument. In this way, the items in dock recent list match VSCode recent list in reverse order. |
@malingyan2017 not sure this fixes the issue in a good way. I think I would expect the most recent entry in the dock to be to the top and with your change it would be reverse. Also, if the user configured the number of entries to be larger than our maximum, the user would end up with a list of old entries, but not new. |
@misolori quick question: do you know of a macOS UX guideline that explains in which order recent documents should appear in the dock? Currently we put the most recent to the top of that list, but arguably since the menu typically opens to the top, the most recent entry is far away from the mouse, so I wonder if it should be the other way around. |
@bpasero Here is the link to MacOS UX Guidelines - https://developer.apple.com/design/human-interface-guidelines/macos/menus/menu-anatomy/ |
@bpasero i follow this post https://stackoverflow.com/questions/17522286/is-there-a-way-to-get-all-values-in-nsuserdefaults, it doesn't include "com.apple.LSShareFileList.MaxAmount" as a key. I check Apple Pages and Microsoft Word behavior. Apple Pages most recent doc actually is at the bottom. Microsoft Word uses a secondary list and the most recent is at the top.
|
@malingyan2017 when I read the Apple guidelines it reads:
It seems that the way we do it is correct given that statement. I will check your updated PR. It would still be great if we could just read the setting. I am worried that we are potentially calling the native API to add entries a ton of times for all the files.
Do you see anything similar at least? |
Hm, actually VLC on macOS also puts the most recent item to the bottom of the menu, that kind of convinces me that we are doing it wrong :) Update: also TextEdit! yeah we should change our sorting! |
I take a step back, and look at the code for Windows. It calls app.addRecentDocument in HistoryMainService.addRecentlyOpened for file, and doesn't remove the entry for Windows in removeFromRecentlyOpened. Two questions:
I updated the PR to unify macOS and Windows. Not verified on Windows. |
@malingyan2017 I am not sure I understand your latest changes, I think the Windows task item list is perfectly fine. I just checked with other apps on Windows (e.g. Notepad, WordPad) and the order of documents is from top to bottom with the most recent document to the top (same as we do). For macOS I have a change ready now that I think works well based on your previous work, maybe you can take a look and adopt it for your PR so that we can merge it. The strategy is:
Given that, even with a small configured number of recent documents, we make sure that the most recent workspaces still appear (which imho are the most important entries).
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as nls from 'vs/nls';
import * as arrays from 'vs/base/common/arrays';
import { IStateService } from 'vs/platform/state/common/state';
import { app } from 'electron';
import { ILogService } from 'vs/platform/log/common/log';
import { getBaseLabel, getPathLabel } from 'vs/base/common/labels';
import { IPath } from 'vs/platform/windows/common/windows';
import { Event as CommonEvent, Emitter } from 'vs/base/common/event';
import { isWindows, isMacintosh } from 'vs/base/common/platform';
import { IWorkspaceIdentifier, IWorkspacesMainService, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces';
import { IHistoryMainService, IRecentlyOpened, isRecentWorkspace, isRecentFolder, IRecent, isRecentFile, IRecentFolder, IRecentWorkspace, IRecentFile } from 'vs/platform/history/common/history';
import { ThrottledDelayer } from 'vs/base/common/async';
import { isEqual as areResourcesEqual, dirname, originalFSPath, basename } from 'vs/base/common/resources';
import { URI } from 'vs/base/common/uri';
import { Schemas } from 'vs/base/common/network';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { getSimpleWorkspaceLabel } from 'vs/platform/label/common/label';
import { toStoreData, restoreRecentlyOpened, RecentlyOpenedStorageData } from 'vs/platform/history/electron-main/historyStorage';
import { exists } from 'vs/base/node/pfs';
import { ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation';
import { ILifecycleService, LifecycleMainPhase } from 'vs/platform/lifecycle/electron-main/lifecycleMain';
export class HistoryMainService implements IHistoryMainService {
private static readonly MAX_TOTAL_RECENT_ENTRIES = 100;
private static readonly MAX_MACOS_DOCK_RECENT_WORKSPACES = 7; // prefer more workspaces...
private static readonly MAX_MACOS_DOCK_RECENT_FILES = 3; // ...compared to files
// Exclude some very common files from the dock/taskbar
private static readonly COMMON_FILES_FILTER = [
'COMMIT_EDITMSG',
'MERGE_MSG'
];
private static readonly recentlyOpenedStorageKey = 'openedPathsList';
_serviceBrand: ServiceIdentifier<IHistoryMainService>;
private _onRecentlyOpenedChange = new Emitter<void>();
readonly onRecentlyOpenedChange: CommonEvent<void> = this._onRecentlyOpenedChange.event;
private macOSRecentDocumentsUpdater: ThrottledDelayer<void>;
constructor(
@IStateService private readonly stateService: IStateService,
@ILogService private readonly logService: ILogService,
@IWorkspacesMainService private readonly workspacesMainService: IWorkspacesMainService,
@IEnvironmentService private readonly environmentService: IEnvironmentService,
@ILifecycleService lifecycleService: ILifecycleService
) {
this.macOSRecentDocumentsUpdater = new ThrottledDelayer<void>(800);
lifecycleService.when(LifecycleMainPhase.AfterWindowOpen).then(() => this.handleWindowsJumpList());
}
private handleWindowsJumpList(): void {
if (!isWindows) {
return; // only on windows
}
this.updateWindowsJumpList();
this.onRecentlyOpenedChange(() => this.updateWindowsJumpList());
}
addRecentlyOpened(newlyAdded: IRecent[]): void {
const workspaces: Array<IRecentFolder | IRecentWorkspace> = [];
const files: IRecentFile[] = [];
for (let curr of newlyAdded) {
// Workspace
if (isRecentWorkspace(curr)) {
if (!this.workspacesMainService.isUntitledWorkspace(curr.workspace) && indexOfWorkspace(workspaces, curr.workspace) === -1) {
workspaces.push(curr);
}
}
// Folder
else if (isRecentFolder(curr)) {
if (indexOfFolder(workspaces, curr.folderUri) === -1) {
workspaces.push(curr);
}
}
// File
else {
const alreadyExistsInHistory = indexOfFile(files, curr.fileUri) >= 0;
const shouldBeFiltered = curr.fileUri.scheme === Schemas.file && HistoryMainService.COMMON_FILES_FILTER.indexOf(basename(curr.fileUri)) >= 0;
if (!alreadyExistsInHistory && !shouldBeFiltered) {
files.push(curr);
// Add to recent documents (Windows only, macOS later)
if (isWindows && curr.fileUri.scheme === Schemas.file) {
app.addRecentDocument(curr.fileUri.fsPath);
}
}
}
}
this.addEntriesFromStorage(workspaces, files);
if (workspaces.length > HistoryMainService.MAX_TOTAL_RECENT_ENTRIES) {
workspaces.length = HistoryMainService.MAX_TOTAL_RECENT_ENTRIES;
}
if (files.length > HistoryMainService.MAX_TOTAL_RECENT_ENTRIES) {
files.length = HistoryMainService.MAX_TOTAL_RECENT_ENTRIES;
}
this.saveRecentlyOpened({ workspaces, files });
this._onRecentlyOpenedChange.fire();
// Schedule update to recent documents on macOS dock
if (isMacintosh) {
this.macOSRecentDocumentsUpdater.trigger(() => this.updateMacOSRecentDocuments());
}
}
removeFromRecentlyOpened(toRemove: URI[]): void {
const keep = (recent: IRecent) => {
const uri = location(recent);
for (const r of toRemove) {
if (areResourcesEqual(r, uri)) {
return false;
}
}
return true;
};
const mru = this.getRecentlyOpened();
const workspaces = mru.workspaces.filter(keep);
const files = mru.files.filter(keep);
if (workspaces.length !== mru.workspaces.length || files.length !== mru.files.length) {
this.saveRecentlyOpened({ files, workspaces });
this._onRecentlyOpenedChange.fire();
// Schedule update to recent documents on macOS dock
if (isMacintosh) {
this.macOSRecentDocumentsUpdater.trigger(() => this.updateMacOSRecentDocuments());
}
}
}
private async updateMacOSRecentDocuments(): Promise<void> {
if (!isMacintosh) {
return;
}
// We clear all documents first to ensure an up-to-date view on the set. Since entries
// can get deleted on disk, this ensures that the list is always valid
app.clearRecentDocuments();
const mru = this.getRecentlyOpened();
// Collect max-N recent workspaces that are known to exist
const workspaceEntries: string[] = [];
for (let i = 0, entries = 0; i < mru.workspaces.length && entries < HistoryMainService.MAX_MACOS_DOCK_RECENT_WORKSPACES; i++) {
const loc = location(mru.workspaces[i]);
if (loc.scheme === Schemas.file) {
const workspacePath = originalFSPath(loc);
if (await exists(workspacePath)) {
workspaceEntries.push(workspacePath);
entries++;
}
}
}
// Collect max-N recent files that are known to exist
const fileEntries: string[] = [];
for (let i = 0, entries = 0; i < mru.files.length && entries < HistoryMainService.MAX_MACOS_DOCK_RECENT_FILES; i++) {
const loc = location(mru.files[i]);
if (loc.scheme === Schemas.file) {
const filePath = originalFSPath(loc);
if (
HistoryMainService.COMMON_FILES_FILTER.indexOf(basename(loc)) !== -1 || // skip some well known file entries
workspaceEntries.indexOf(filePath) !== -1 // prefer a workspace entry over a file entry (e.g. for .code-workspace)
) {
continue;
}
if (await exists(filePath)) {
fileEntries.push(filePath);
entries++;
}
}
}
// The apple guidelines (https://developer.apple.com/design/human-interface-guidelines/macos/menus/menu-anatomy/)
// explain that most recent entries should appear close to the interaction by the user (e.g. close to the
// mouse click). Most native macOS applications that add recent documents to the dock, show the most recent document
// to the bottom (because the dock menu is not appearing from top to bottom, but from the bottom to the top). As such
// we fill in the entries in reverse order so that the most recent shows up at the bottom of the menu.
//
// On top of that, the maximum number of documents can be configured by the user (defaults to 10). To ensure that
// we are not failing to show the most recent entries, we start by adding files first (in reverse order of recency)
// and then add folders (in reverse order of recency). Given that strategy, we can ensure that the most recent
// N folders are always appearing, even if the limit is low (https://github.com/microsoft/vscode/issues/74788)
fileEntries.reverse().forEach(fileEntry => app.addRecentDocument(fileEntry));
workspaceEntries.reverse().forEach(workspaceEntry => app.addRecentDocument(workspaceEntry));
}
clearRecentlyOpened(): void {
this.saveRecentlyOpened({ workspaces: [], files: [] });
app.clearRecentDocuments();
// Event
this._onRecentlyOpenedChange.fire();
}
getRecentlyOpened(currentWorkspace?: IWorkspaceIdentifier, currentFolder?: ISingleFolderWorkspaceIdentifier, currentFiles?: IPath[]): IRecentlyOpened {
const workspaces: Array<IRecentFolder | IRecentWorkspace> = [];
const files: IRecentFile[] = [];
// Add current workspace to beginning if set
if (currentWorkspace && !this.workspacesMainService.isUntitledWorkspace(currentWorkspace)) {
workspaces.push({ workspace: currentWorkspace });
}
if (currentFolder) {
workspaces.push({ folderUri: currentFolder });
}
// Add currently files to open to the beginning if any
if (currentFiles) {
for (let currentFile of currentFiles) {
const fileUri = currentFile.fileUri;
if (fileUri && indexOfFile(files, fileUri) === -1) {
files.push({ fileUri });
}
}
}
this.addEntriesFromStorage(workspaces, files);
return { workspaces, files };
}
private addEntriesFromStorage(workspaces: Array<IRecentFolder | IRecentWorkspace>, files: IRecentFile[]) {
// Get from storage
let recents = this.getRecentlyOpenedFromStorage();
for (let recent of recents.workspaces) {
let index = isRecentFolder(recent) ? indexOfFolder(workspaces, recent.folderUri) : indexOfWorkspace(workspaces, recent.workspace);
if (index >= 0) {
workspaces[index].label = workspaces[index].label || recent.label;
} else {
workspaces.push(recent);
}
}
for (let recent of recents.files) {
let index = indexOfFile(files, recent.fileUri);
if (index >= 0) {
files[index].label = files[index].label || recent.label;
} else {
files.push(recent);
}
}
}
private getRecentlyOpenedFromStorage(): IRecentlyOpened {
const storedRecents = this.stateService.getItem<RecentlyOpenedStorageData>(HistoryMainService.recentlyOpenedStorageKey);
return restoreRecentlyOpened(storedRecents);
}
private saveRecentlyOpened(recent: IRecentlyOpened): void {
const serialized = toStoreData(recent);
this.stateService.setItem(HistoryMainService.recentlyOpenedStorageKey, serialized);
}
updateWindowsJumpList(): void {
if (!isWindows) {
return; // only on windows
}
const jumpList: Electron.JumpListCategory[] = [];
// Tasks
jumpList.push({
type: 'tasks',
items: [
{
type: 'task',
title: nls.localize('newWindow', "New Window"),
description: nls.localize('newWindowDesc', "Opens a new window"),
program: process.execPath,
args: '-n', // force new window
iconPath: process.execPath,
iconIndex: 0
}
]
});
// Recent Workspaces
if (this.getRecentlyOpened().workspaces.length > 0) {
// The user might have meanwhile removed items from the jump list and we have to respect that
// so we need to update our list of recent paths with the choice of the user to not add them again
// Also: Windows will not show our custom category at all if there is any entry which was removed
// by the user! See https://github.com/Microsoft/vscode/issues/15052
let toRemove: URI[] = [];
for (let item of app.getJumpListSettings().removedItems) {
const args = item.args;
if (args) {
const match = /^--(folder|file)-uri\s+"([^"]+)"$/.exec(args);
if (match) {
toRemove.push(URI.parse(match[2]));
}
}
}
this.removeFromRecentlyOpened(toRemove);
// Add entries
jumpList.push({
type: 'custom',
name: nls.localize('recentFolders', "Recent Workspaces"),
items: arrays.coalesce(this.getRecentlyOpened().workspaces.slice(0, 7 /* limit number of entries here */).map(recent => {
const workspace = isRecentWorkspace(recent) ? recent.workspace : recent.folderUri;
const title = recent.label || getSimpleWorkspaceLabel(workspace, this.environmentService.untitledWorkspacesHome);
let description;
let args;
if (isSingleFolderWorkspaceIdentifier(workspace)) {
description = nls.localize('folderDesc', "{0} {1}", getBaseLabel(workspace), getPathLabel(dirname(workspace), this.environmentService));
args = `--folder-uri "${workspace.toString()}"`;
} else {
description = nls.localize('workspaceDesc', "{0} {1}", getBaseLabel(workspace.configPath), getPathLabel(dirname(workspace.configPath), this.environmentService));
args = `--file-uri "${workspace.configPath.toString()}"`;
}
return {
type: 'task',
title,
description,
program: process.execPath,
args,
iconPath: 'explorer.exe', // simulate folder icon
iconIndex: 0
};
}))
});
}
// Recent
jumpList.push({
type: 'recent' // this enables to show files in the "recent" category
});
try {
app.setJumpList(jumpList);
} catch (error) {
this.logService.warn('#setJumpList', error); // since setJumpList is relatively new API, make sure to guard for errors
}
}
}
function location(recent: IRecent): URI {
if (isRecentFolder(recent)) {
return recent.folderUri;
}
if (isRecentFile(recent)) {
return recent.fileUri;
}
return recent.workspace.configPath;
}
function indexOfWorkspace(arr: IRecent[], workspace: IWorkspaceIdentifier): number {
return arrays.firstIndex(arr, w => isRecentWorkspace(w) && w.workspace.id === workspace.id);
}
function indexOfFolder(arr: IRecent[], folderURI: ISingleFolderWorkspaceIdentifier): number {
return arrays.firstIndex(arr, f => isRecentFolder(f) && areResourcesEqual(f.folderUri, folderURI));
}
function indexOfFile(arr: IRecentFile[], fileURI: URI): number {
return arrays.firstIndex(arr, f => areResourcesEqual(f.fileUri, fileURI));
} |
@bpasero thanks very much for the code. I made one change on top yours: Sorry, I was wrong about the Windows. Get a Windows machine, and find VSCode slit workspace and files nicely in the Recent Document List. Follow up on macOS question: I was asking if we can just simply using app.addRecentDocument to add entries (can be workspace or file) whenever a document is opened in VSCode. I think this will get your original suggestion ("we can think about making the list smarter by just showing entries based on recency..."). Is it a requirement to show two lists (group workspace together and show at the bottom, then files)? |
Thanks, I merged this in. To summarise the changes:
I decided not to filter duplicates in the list because native mac apps also do not do this and there does not seem to exist any API on macOS to distinguish them. |
…5108) * Fix Recent list in dock does not show recent files/folders #74788 macOS only shows recent list with max number based on sytem config. It takes bottom n entries from the list added through Electron app.addRecentDocument. To match macOS dock recent list with VSCode internal recent list, this change add items into macOS recent document list in reverse order of internal recent list. * remove dock recent entries limit * add entry of folder (5) back to allow show some files * fix showsing wrong folders in recentlist * simplify adding platform RecentDocument in macOS * update based on bpasero suggestion * set max entries * rename macro
related to #57272 but the dock context menu show seemingly random items, surely nothing recent (where is vscode) and apparently also duplicates (2x extensions)
If we cannot get this to work properly can we at least add an option to not have this feature? It always disappoints me and leaves the sore feeling that our list is not good while other (mac) apps have this working flawless.
The text was updated successfully, but these errors were encountered: