-
Notifications
You must be signed in to change notification settings - Fork 2.5k
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
multi-root workspace support, minimal patch #2117
Conversation
37f2c75
to
a98a351
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I might have missed some things but it looks nice !
@@ -124,6 +131,10 @@ export class FileTreeModel extends TreeModelImpl implements LocationService { | |||
await this.fileSystem.move(sourceUri, targetUri, { overwrite: true }); | |||
// to workaround https://github.com/Axosoft/nsfw/issues/42 | |||
this.refresh(target); | |||
|
|||
if (source.parent) { | |||
this.refresh(source.parent); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This wasn't here before, is it a bug fix packed along the multi-root feature ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Correct me if i am wrong: i believe this is related to Axosoft/nsfw#42.
When files move from one root to the other, the code is not getting enough information to find out the source.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the explanation !
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you elaborate on how can I reproduce an issue without this code? refresh
supposed to handle Axosoft/nsfw#42.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@elaihau could you comment on previous comment please
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry I missed it. The way to reproduce it is having more than one root folders added to the workspace (say, folder1
and folder2
), and moving a folder by drag-and-drop from folder2
to folder1
, and same folder back from folder1
to folder2
.
@@ -78,6 +90,38 @@ export class FileTree extends TreeImpl { | |||
}; | |||
} | |||
|
|||
protected isVirtualRoot(node: CompositeTreeNode): boolean { | |||
return node.parent === undefined && this.hasVirtualRoot && node.name === 'WorkspaceRoot'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it possible to simply test the node based on a field it has ?
Like instead of checking if the tree knows if it holds a virtual root and check the name, have a field virtual
and test like !node.parent && node.virtual
. Or even !node.parent && VirtualNode.is(node)
to match the way the check is done on other nodes ?
Maybe the tree doesn't have to know if it holds a virtual node, that's my thought. Tell me if I missed something.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe I don't event need the flag hasVirtualRoot
in FileTree
class. Like you said, the tree shouldn't have the knowledge pertaining to whether its root is virtual or not.
I will make the change you suggested.
await this.refreshWidget(workspaceChangeData.root, workspaceChangeData.roots, workspaceChangeData.workspaceConfigFile); | ||
} | ||
|
||
protected async refreshWidget(root: FileStat | undefined, roots: FileStat[], workspaceConfig: FileStat | undefined) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it be better to just rely on roots
and not root
at the same time ?
I feel like the prototype should be:
refreshWidget(roots: FileStat[], workspaceConfig: FileStat | undefined)
this.startWatch(valid); | ||
this.onWorkspaceRootChangeEmitter.fire(this._workspaceRootChange); | ||
} | ||
return Promise.resolve(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
return;
@@ -19,7 +19,8 @@ export class WorkspaceUriLabelProviderContribution extends DefaultUriLabelProvid | |||
constructor(@inject(WorkspaceService) wsService: WorkspaceService, | |||
@inject(FileSystem) protected fileSystem: FileSystem) { | |||
super(); | |||
wsService.root.then(root => { | |||
wsService.workspaceRootChange.then(data => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is minor, but maybe this could be in an async
postConstruct method, to convert this part to attribute injection like the documentation suggests to do ?
newConfig.roots = [workspaceFolderUri, rootUriToAdd]; | ||
} | ||
await this.writeToWorkspaceFolder(newConfig); | ||
return Promise.resolve(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can remove this line unless you think it adds clarity.
if (configContent) { | ||
await this.writeToWorkspaceFolder({ roots: configContent.roots.filter(r => r !== rootUriToRemove) }); | ||
} | ||
return Promise.resolve(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can remove this line unless you think it adds clarity.
Also for some reason, when I finish to build your branch, I see that there is a diff on |
firstRootNameDiv.textContent = rootName; | ||
firstRootMsg.appendChild(firstRootNameDiv); | ||
const remainder = document.createElement('span'); | ||
remainder.textContent = ' and its subfolders cannot be removed as it is the first root folder of the workspace.'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As discussed, it may no longer be necessary to treat the first root folder in a special way, at least as far as removal goes.
I tried this and was not able to switch workspace, using "File -> open". It worked when I first closed the current workspace and tried again. |
@elaihau I think the description of this PR and commit message should be updated/enhanced. Here's my draft suggestion; we can discuss tomorrow. What is a workspace / workspace folder? A folder on the file system that serves as a container for:
Theia and its extensions will generally only consider folders that are part of the
For extensions that are not yet updated to support multiple root folders, they will temporarily default to using the first configured root folder as fallback. For backward compatibility and simplicity, a workspace folder that contains no explicit root folder list configuration will use its own folder as the single root. This permits using any folder as a workspace. What's in this PR:
|
One little bug I noticed: If I enable the preference, I can see and execute the multi-root related commands. If I then disable the preference, the commands are still visible but disabled, until I restart the client. After client restart, the commands are no longer visible. Update: not sure it's always the case, but when I just added the preference globally, I needed a client restart before I could see the commands in the context menu, |
Looking at the following statement mentioned before:
If subsequently an additional root folder is added to such a workspace, then both the newly added folder and the original implicit root are added to the root folders list configuration. Either can later be removed. If all roots are removed form the configuration, the workspace falls-back to using its own folder as implicit root.
For the initial contribution, will we be able to remove the initial root folders !!
…________________________________
De : marcdumais-work <notifications@github.com>
Envoyé : 26 juin 2018 20:58:07
À : theia-ide/theia
Cc : Subscribed
Objet : Re: [theia-ide/theia] multi-root workspace support, minimal patch (#2117)
@elaihau<https://github.com/elaihau> I think the description of this PR and commit message should be updated/enhanced. Here's my draft suggestion; we can discuss tomorrow.
________________________________
What is a workspace / workspace folder? A folder on the file system that serves as a container for:
* workspace configuration: in sub-folder .theia
* preferences, in .theia/settings.json
* root folders list, in .theia/roots.json
* optionally content in the form of other files/folders
Theia and its extensions will generally only consider folders that are part of the root folders list. e.g.:
* will be shown in the file navigator
* will be scanned for git repositories that the user can select, stage and commit files into
* [similarly for other extensions, to be updated later]
For extensions that are not yet updated to support multiple root folders, they will temporarily default to using the first configured root folder as fallback.
For backward compatibility and simplicity, a workspace folder that does not contain no explicit root folder list configuration will use its own folder as the single root. This permits using any folder as a workspace.
If subsequently an additional root folder is added to such a workspace, then both the newly added folder and the original implicit root are added to the root folders list configuration. Either can later be removed. If all roots are removed form the configuration, the workspace falls-back to using its own folder as implicit root.
What's in this PR:
* a workspace's root folders list can be modified using the navigator context menu "add/remove folder" items to add or remove a folder
* preference 'workspace.supportMultiRootWorkspace', with default value false, can be used to control if the context menu entries, used to modify the list of root folders, are shown or not. Since this is a new feature that is not supported by most Theia extensions yet, it probably makes sense to hide the UI by default for now
* previously the "current root" was a static value, obtained by each extension that needs it, at the startup of a Theia client. To change it (e.g. to switch workspace), a restart of the client was required. For this feature we did not want to have to restart the client each time the list of root folders is updated, so we introduced a new "onWorkspaceRootChanged" event. Extensions that need to know when the list of root folders is modified can react to this event, e.g. to update their widgets. It's still necessary to reload the Theia frontend when switching workspace in a given tab or when closing a workspace.
* extensions updated: workspace, git, & navigator
—
You are receiving this because you are subscribed to this thread.
Reply to this email directly, view it on GitHub<#2117 (comment)>, or mute the thread<https://github.com/notifications/unsubscribe-auth/AAZk69UDp3Eo6W4jpreDpF1VuD0eM9Ccks5uAtifgaJpZM4UqIyF>.
|
Yes, with a couple of minor adjustments to the current PR. |
const parent = this.getNode(change.uri.parent.toString()); | ||
if (DirNode.is(parent) && parent.expanded) { | ||
accept(parent); | ||
const parentNodes = this.getNodes((node: FileStatNode) => change.uri.parent.toString() === node.uri.toString()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a way to look up by an id still? An idea was to have fast look up, now it seems to travers the whole tree to find a node.
@@ -204,6 +208,17 @@ export class TreeImpl implements Tree { | |||
return id !== undefined ? this.nodes[id] : undefined; | |||
} | |||
|
|||
getNodes(test: (node: TreeNode) => boolean): TreeNode[] { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks like a very expensive operation. Is there a way to replace clients with getNode(id)
by redesigning their tree structure?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Then what id
would you like to use ? Two different nodes can hold the same file uri (which was the old id).
example: I have the following roots:
- /home/x/projects/theia/
- /home/x/projects/
(it is something possible on vscode)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
getNode(id)
function is still in the interface. getNodes()
is only called when there is a need. Having multiple roots opened in the file navigator means there could be more than one file associated with the same uri (like the scenario that @marechal-p described above).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suppose there is file://foo/bar/baz/x.txt
file, and 2 workspaces file://foo
and file://foo/bar
.
If a file tree node have an id of form ${workspaceUri}:${fileUri}
or something like that (so file://foo:file://foo/bar/baz/x.txt
and file://foo/bar:file://foo/bar/baz/x.txt
), then when a file is changed one can take a list of all workspaces, loop over them to create node ids and call getNode
for each.
I assume that a list of workspaces will be short in most of cases (i have maximum 10 at the time), in comparison the file tree only for node_modules
folder has over 800 children.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Having an id in the form of ${workspaceUri}:${fileUri}
is very close to I already did in this PR. With the ${workspaceUri}
part of the id, we could easily figure out which root folder that the node comes from. No issues. :)
The getNodes()
function is only called in the scenario where the ${workspaceUri}:${fileUri}
is unavailable. Imagine we have file://foo:file://foo/bar/baz/x.txt
and file://foo/bar:file://foo/bar/baz/x.txt
in the workspace, and a user opens the file://foo/bar/baz/x.txt
from the quick open (Ctrl + p
). When this happens we have no context of this x.txt
and therefore would have no idea which node to highlight in the file tree.
I see the reason that you think this operation is expensive. I would be happy if we could find a work around of calling getNodes()
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When this happens we have no context of this x.txt and therefore would have no idea which node to highlight in the file tree.
I don't understand how getNodes()
makes difference? We always know workspace roots, so it is not an issue to create file://foo:file://foo/bar/baz/x.txt
and file://foo/bar:file://foo/bar/baz/x.txt
and call getNode
for them.
Also sorry i meant ${workspaceRootUri}:${fileUri}
, not ${workspaceUri}:...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Concatenating the absolute root uris with the absolute file uris seems to be the most simple way to craft an id indeed. The getNodes
was to make the loop you talked about part of the implementation, but thinking back it doesn't make really much sense for the Tree
interface indeed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@akosyakov fixed
this.update(); | ||
const icon = await this.labelProvider.getIcon(root); | ||
this.model.root = DirNode.createRoot(root, label, icon); | ||
} else if (roots.length > 1) { // more than one folder |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ideally having a single root in the workspace is just one scenario that multi-root workspace supports.
I actually don't have to have
if (roots.length === 1) { // only one folder is opened
...
else if (roots.length > 1) { // more than one folder
And the logic under the else if
should be good enough to have the file tree displayed in both cases.
The reason that I am separating the one-root
and multi-root
is that, the file tree would collapse due to #1781 and the tree wouldn't look as nice as it should, in the multi-root setting.
I will keep the single root
and multi root
display logic separated until #1781 is fixed.
@elaihau as briefly discussed, the event added to notify extensions that the workspace has changed (added or removed a root folder) seems to be sent only in the frontend where it was triggered.So if we have more than one client using that workspace, the other one(s) will not know to update their widgets/state to reflect the change. e.g. the navigator of other clients will not show the latest roots |
b8869ba
to
a0248a5
Compare
@@ -121,6 +121,10 @@ export class FileTreeModel extends TreeModelImpl implements LocationService { | |||
* Move the given source file or directory to the given target directory. | |||
*/ | |||
async move(source: TreeNode, target: TreeNode) { | |||
if (source.parent && source.parent.name === 'WorkspaceRoot') { | |||
// do not support moving the root folder |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That tests for any root folder, correct? If so, the comment could be adjusted. i.e. "the root" -> "a root"
|
||
if (validUris.length > 0 && this._root) { | ||
const messageContainer = document.createElement('div'); | ||
messageContainer.textContent = 'Do you really want to remove the following roots and their subfolders from the workspace?'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would some users be scared that the content of the removed roots might be deleted? Since there is already an appropriate title on the dialog (Remove Folder From Workspace), maybe we could shorten the message and mention nothing is physically erased? e.g.:
"Remove the following folders from workspace? (note: nothing will be erased from disk)"
agreed. settings from different root folders should be separated. There will be a separated PR in near future that addresses preference issues introduced by having multiple roots in the same workspace. |
7027290
to
d153165
Compare
if (rootStat) { | ||
this._rootUri = rootStat.uri; | ||
this.resolveReady(); | ||
} | ||
}); | ||
this.workspaceService.onWorkspaceChanged(event => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
workspaceFolder
cannot change right?
return CompositeTreeNode.is(node) && node.name === WorkspaceNode.name; | ||
} | ||
|
||
export function createRoot(children: DirNode[]): WorkspaceNode { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
DirNode
-> WorkspaceRootNode
) { } | ||
|
||
registerCommands(commands: CommandRegistry): void { | ||
commands.registerCommand(WorkspaceCommands.OPEN, { | ||
isEnabled: () => true, | ||
execute: () => this.showFileDialog() | ||
execute: () => this.workspaceService.workspace.then(async data => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
please extract a protected method showFileDialog
as before
constructor(@inject(WorkspaceService) wsService: WorkspaceService, | ||
@inject(FileSystem) protected fileSystem: FileSystem) { | ||
|
||
constructor() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can be removed?
please rebase the PR, build failures probably are caused by a new release please also look at #2117 (comment) |
done.
just did the rebase. let me see if it builds this time |
In that case, we could simply use roots[0] for now to obtain the location of preferences. |
You might want to argue that "we could expose both |
I don't really understand why it would be proper to use the But since you want to keep this PR small, we should tackle the preferences separately. |
In this use case
That would introduce inconsistencies if we don't change
what i dislike is the 2) and 3) mentioned above, where theia settings silently change after the browser refresh. |
I'm confused, we don't have something like folder settings, do we? |
at the moment we don't have folder settings. it is something that I am going to add.
because not all theia extensions are updated (as of today) to embrace the multi-root change. And this is why i think "go with the first root, what ever that is" wouldn't keep this PR small:
free feel to disagree: i think keeping the we could discuss this in tomorrow's meeting, if you would be there :) |
71a12e5
to
a0fe5c5
Compare
@@ -74,14 +77,21 @@ export class GitRepositoryProvider { | |||
} | |||
|
|||
async refresh(options?: GitRefreshOptions): Promise<void> { | |||
const root = await this.workspaceService.root; | |||
const roots = await this.workspaceService.roots; | |||
const root = roots[0]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The test for the first root should be removed. We are dealing with an array, which might be empty, but that's fine.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you are right. changes done.
const root = await this.workspaceService.root; | ||
if (root) { | ||
const rootUri = new URI(root.uri); | ||
const workspaceFolder = (await this.workspaceService.roots)[0]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we put a TODO comment to all the locations where we just fetch the first root, so we don't miss them?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
no worries :) I will review all references of WorkspaceService
before creating my next PR
I tested my change on top of the latest master, and looks like the tree navigation works. Please see the attached GIF. Could you please tell me how to reproduce the problem you observed? Maybe i have missed something in my test. Thanks ! |
3ea11aa
to
d4896cf
Compare
What is a workspace / workspace folder? It is a folder on the file system that serves as a container for: - workspace configuration: in sub-folder ".theia" - preferences, in ".theia/settings.json" - root folders list, in ".theia/roots.json" - optionally content in the form of other files/folders For backward compatibility and simplicity, a workspace folder that contains no explicit root folder list configuration will use its own folder as the single root. This permits using any folder as a workspace. If subsequently an additional root folder is added to such a workspace, then both the newly added folder and the original implicit root are added to the root folders list configuration. Either can later be removed. If all roots are removed form the configuration, the workspace falls-back to using its own folder as implicit root. This PR is the minimal patch for eclipse-theia#1660 What's included in this PR: - a workspace's root folders list could be modified using the navigator context menu "add/remove folder" items to add or remove a folder - preference 'workspace.supportMultiRootWorkspace', with default value false, can be used to control if the context menu entries, used to modify the list of root folders, are shown or not. Since this is a new feature that is not supported by most Theia extensions yet, it probably makes sense to hide the UI by default for now - previously the "current root" was a static value, obtained by each extension that needs it, at the startup of a Theia client. To change it (e.g. to switch workspace), a restart of the client was required. For this feature we did not want to have to restart the client each time the list of root folders is updated, so we introduced a new "onWorkspaceRootChanged" event. Extensions that need to know when the list of root folders is modified can react to this event, e.g. to update their widgets. It's still necessary to reload the Theia frontend when switching workspace in a given tab or when closing a workspace. - the root folder list comprised of 1 to many folders can be displayed from the file navigator widget. - the root folders will be scanned for git repositories that the user can select, stage and commit files into. What's not included: - users should have the choice of naming the workspace and choosing where to store the config file - preference extension should be able to handle the settings from multiple projects - search-in-workspace and file-search extension should be able to perform the search across all folders in the workspace, and / or provide filtering mechanism to reduce the amount of information being displayed - output and problems widgets should display data from all root folders, and / or provide filtering mechanism to reduce the amount of information being displayed - task extension should be able to run tasks defined under all root folders - users should be able to open the terminal from any root folder in the workspace Signed-off-by: elaihau <liang.huang@ericsson.com>
What is a workspace / workspace folder? It is a folder on the file system that serves as a container for:
.theia
.theia/settings.json
.theia/roots.json
For backward compatibility and simplicity, a workspace folder that contains no explicit root folder list configuration will use its own folder as the single root. This permits using any folder as a workspace.
If subsequently an additional root folder is added to such a workspace, then both the newly added folder and the original implicit root are added to the root folders list configuration. Either can later be removed. If all roots are removed form the configuration, the workspace falls-back to using its own folder as implicit root.
This PR is the minimal patch for #1660
What's included in this PR:
What's not included:
Signed-off-by: elaihau liang.huang@ericsson.com