diff --git a/che-theia-java-extension/src/browser/action/mark-dir-as-source.ts b/che-theia-java-extension/src/browser/action/mark-dir-as-source.ts new file mode 100644 index 0000000..8d12c5b --- /dev/null +++ b/che-theia-java-extension/src/browser/action/mark-dir-as-source.ts @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2012-2018 Red Hat, Inc. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which is available at http://www.eclipse.org/legal/epl-2.0.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ + +import { inject, injectable } from "inversify"; +import { ClasspathContainer, ClasspathEntry, ClasspathEntryKind } from "../classpath/classpath-container"; +import { CommandContribution, MenuContribution, SelectionService, CommandRegistry, MenuModelRegistry, Command } from "@theia/core"; +import { UriAwareCommandHandler, UriCommandHandler } from "@theia/core/lib/common/uri-command-handler"; +import URI from "@theia/core/lib/common/uri"; +import { NAVIGATOR_CONTEXT_MENU } from "@theia/navigator/lib/browser/navigator-contribution"; +import { CompositeTreeNode, WidgetManager } from "@theia/core/lib/browser"; +import { WorkspaceService } from "@theia/workspace/lib/browser"; +import { FileNavigatorWidget, FILE_NAVIGATOR_ID } from "@theia/navigator/lib/browser/navigator-widget"; +import { JavaUtils } from "../java-utils"; +import { SourceView } from "../classpath/pages/source/source-view"; + + +export const MARKSOURCEDIR = [...NAVIGATOR_CONTEXT_MENU, '7_sourcedir']; + + +export namespace JavaCommands { + export const MARKSOURCEDIR: Command = { + id: 'java:mark-source-dir', + label: 'Mark Dir as Source' + }; +} + +@injectable() +export class MarkDirAsSourceAction implements CommandContribution, MenuContribution { + + constructor(@inject(ClasspathContainer) protected readonly classpathContainer: ClasspathContainer, + @inject(SelectionService) protected readonly selectionService: SelectionService, + @inject(WidgetManager) protected readonly widgetManager: WidgetManager, + @inject(WorkspaceService) protected readonly workspaceService: WorkspaceService, + @inject(SourceView) protected readonly sourceView: SourceView) { + } + + async performAction(projectURI: string, treeNodeID: string) { + const classpathItems = await this.classpathContainer.getClassPathEntries(projectURI); + const newClasspathItem = { + children: [], + entryKind: ClasspathEntryKind.SOURCE, + path: JavaUtils.getIDFromMultiRootID(treeNodeID) + } as ClasspathEntry + classpathItems.push(newClasspathItem); + this.classpathContainer.resolveClasspathEntries(classpathItems); + this.classpathContainer.updateClasspath(projectURI); + this.sourceView.classpathModel.addClasspathNodes(newClasspathItem); + } + + registerCommands(commands: CommandRegistry): void { + commands.registerCommand(JavaCommands.MARKSOURCEDIR, this.newUriAwareCommandHandler({ + execute: async fileUri => { + const fileWidget = await this.widgetManager.tryGetWidget(FILE_NAVIGATOR_ID) as FileNavigatorWidget; + if (fileWidget) { + + const roots = await this.workspaceService.roots; + const root = JavaUtils.getRootProjectURI(roots, fileUri.toString()); + if (roots && root) { + const multiRootURI = JavaUtils.getMultiRootReadyURI(root, fileUri.toString()); + const treeNode = fileWidget.model.getNode(multiRootURI); + if (treeNode && CompositeTreeNode.is(treeNode)) { + this.performAction(root, treeNode.id); + } + } + } + + } + })); + } + + registerMenus(menus: MenuModelRegistry): void { + menus.registerMenuAction(MARKSOURCEDIR, { + commandId: JavaCommands.MARKSOURCEDIR.id + }); + } + + protected newUriAwareCommandHandler(handler: UriCommandHandler): UriAwareCommandHandler { + return new UriAwareCommandHandler(this.selectionService, handler); + } +} \ No newline at end of file diff --git a/che-theia-java-extension/src/browser/action/unmark-dir-as-source.ts b/che-theia-java-extension/src/browser/action/unmark-dir-as-source.ts new file mode 100644 index 0000000..9455991 --- /dev/null +++ b/che-theia-java-extension/src/browser/action/unmark-dir-as-source.ts @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2012-2018 Red Hat, Inc. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which is available at http://www.eclipse.org/legal/epl-2.0.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ + +import { inject, injectable } from "inversify"; +import { ClasspathContainer } from "../classpath/classpath-container"; +import { CommandContribution, MenuContribution, SelectionService, CommandRegistry, MenuModelRegistry, Command } from "@theia/core"; +import { UriAwareCommandHandler, UriCommandHandler } from "@theia/core/lib/common/uri-command-handler"; +import URI from "@theia/core/lib/common/uri"; +import { CompositeTreeNode, WidgetManager } from "@theia/core/lib/browser"; +import { WorkspaceService } from "@theia/workspace/lib/browser"; +import { FileNavigatorWidget, FILE_NAVIGATOR_ID } from "@theia/navigator/lib/browser/navigator-widget"; +import { JavaUtils } from "../java-utils"; +import { MARKSOURCEDIR } from "./mark-dir-as-source"; +import { SourceView } from "../classpath/pages/source/source-view"; + +export const UNMARKSOURCEDIR: Command = { + id: 'java:unmark-source-dir', + label: 'Unmark Dir as Source' +}; + +@injectable() +export class UnmarkDirAsSourceAction implements CommandContribution, MenuContribution { + + constructor(@inject(ClasspathContainer) protected readonly classpathContainer: ClasspathContainer, + @inject(SelectionService) protected readonly selectionService: SelectionService, + @inject(WidgetManager) protected readonly widgetManager: WidgetManager, + @inject(WorkspaceService) protected readonly workspaceService: WorkspaceService, + @inject(SourceView) protected readonly sourceView: SourceView + ) { + } + + async performAction(projectURI: string, treeNodeID: string) { + const classpathItems = await this.classpathContainer.getClassPathEntries(projectURI); + this.classpathContainer.clearClasspathEntries(); + const realID = JavaUtils.getIDFromMultiRootID(treeNodeID); + const filteredClasspathItems = classpathItems.filter(item => item.path !== realID); + this.classpathContainer.resolveClasspathEntries(filteredClasspathItems); + this.classpathContainer.updateClasspath(projectURI); + this.sourceView.classpathModel.removeClasspathNode(realID); + } + + registerCommands(commands: CommandRegistry): void { + commands.registerCommand(UNMARKSOURCEDIR, this.newUriAwareCommandHandler({ + execute: async fileUri => { + const fileWidget = await this.widgetManager.tryGetWidget(FILE_NAVIGATOR_ID) as FileNavigatorWidget; + if (fileWidget) { + const roots = await this.workspaceService.roots; + const root = JavaUtils.getRootProjectURI(roots, fileUri.toString()); + if (roots && root) { + const multiRootURI = JavaUtils.getMultiRootReadyURI(root, fileUri.toString()); + const treeNode = fileWidget.model.getNode(multiRootURI); + if (treeNode && CompositeTreeNode.is(treeNode)) { + this.performAction(root, treeNode.id); + } + } + } + } + })); + } + + registerMenus(menus: MenuModelRegistry): void { + menus.registerMenuAction(MARKSOURCEDIR, { + commandId: UNMARKSOURCEDIR.id + }); + } + + protected newUriAwareCommandHandler(handler: UriCommandHandler): UriAwareCommandHandler { + return new UriAwareCommandHandler(this.selectionService, handler); + } +} diff --git a/che-theia-java-extension/src/browser/che-theia-java-contribution.ts b/che-theia-java-extension/src/browser/che-theia-java-contribution.ts index ec583d3..e98649e 100644 --- a/che-theia-java-extension/src/browser/che-theia-java-contribution.ts +++ b/che-theia-java-extension/src/browser/che-theia-java-contribution.ts @@ -10,17 +10,42 @@ * Red Hat, Inc. - initial API and implementation */ -import { injectable } from 'inversify'; -import { CommandContribution, CommandRegistry, MenuContribution, MenuModelRegistry } from '@theia/core/lib/common'; -import { KeybindingContribution, KeybindingRegistry } from '@theia/core/lib/browser'; +import { injectable, inject } from "inversify"; +import { CommandContribution, CommandRegistry, MenuContribution, MenuModelRegistry, MAIN_MENU_BAR, Command } from "@theia/core/lib/common"; +import { KeybindingContribution, KeybindingRegistry, WidgetManager } from "@theia/core/lib/browser"; +import { ClassPathDialog } from "./classpath/classpath-dialog"; +import { WorkspaceService } from "@theia/workspace/lib/browser"; + +export const HELP = [...MAIN_MENU_BAR, '5_classpath']; + +export const CONFIGURE_CLASSPATH_COMMAND: Command = { + id: 'java.configure.classpath', + label: 'Configure Classpath' +}; @injectable() export class JavaExtensionContribution implements CommandContribution, MenuContribution, KeybindingContribution { + constructor( + @inject(ClassPathDialog) protected readonly aboutDialog: ClassPathDialog, + @inject(WidgetManager) protected readonly widgetManager: WidgetManager, + @inject(WorkspaceService) protected readonly workspaceService: WorkspaceService) { + } + registerCommands(registry: CommandRegistry): void { + registry.registerCommand(CONFIGURE_CLASSPATH_COMMAND, { + execute: e => { + this.aboutDialog.open(); + } + }); } registerMenus(menus: MenuModelRegistry): void { + menus.registerMenuAction(HELP, { + commandId: CONFIGURE_CLASSPATH_COMMAND.id, + label: CONFIGURE_CLASSPATH_COMMAND.label, + order: '10' + }); } registerKeybindings(keybindings: KeybindingRegistry): void { diff --git a/che-theia-java-extension/src/browser/che-theia-java-frontend-module.ts b/che-theia-java-extension/src/browser/che-theia-java-frontend-module.ts index d12460d..1ddcd28 100644 --- a/che-theia-java-extension/src/browser/che-theia-java-frontend-module.ts +++ b/che-theia-java-extension/src/browser/che-theia-java-frontend-module.ts @@ -17,12 +17,28 @@ import { ResourceResolver } from "@theia/core/lib/common"; -import { ContainerModule } from 'inversify'; -import { KeybindingContribution, KeybindingContext, WidgetFactory} from '@theia/core/lib/browser'; +import { ContainerModule, Container, interfaces } from "inversify"; +import { KeybindingContribution, KeybindingContext, WidgetFactory, TreeProps, createTreeContainer, + defaultTreeProps, TreeWidget, TreeModelImpl, TreeModel } from '@theia/core/lib/browser'; -import '../../src/browser/styles/icons.css'; +import "../../src/browser/styles/icons.css"; +import "../../src/browser/styles/classpath.css"; import { FileStructure } from './navigation/file-structure'; import { JavaEditorTextFocusContext } from './java-keybinding-contexts'; +import { BuildPathTreeWidget, BuildPathTreeWidgetID } from './classpath/build-path-widget'; +import { ClassPathDialog, DialogProps } from './classpath/classpath-dialog'; +import { ClasspathContainer } from './classpath/classpath-container'; +import { SourceModel } from './classpath/pages/source/source-model'; +import { LibraryModel } from './classpath/pages/library/library-model'; +import { ClasspathDecorator } from './classpath/classpath-tree-decorator'; +import { MarkDirAsSourceAction } from './action/mark-dir-as-source'; +import { UnmarkDirAsSourceAction } from './action/unmark-dir-as-source'; +import { NavigatorTreeDecorator } from '@theia/navigator/lib/browser/navigator-decorator-service'; +import { LibraryView, LibraryViewID } from './classpath/pages/library/library-view'; +import { SourceView, SourceViewID } from './classpath/pages/source/source-view'; +import { IClasspathNode } from './classpath/nodes/classpath-node'; +import { LibraryNode } from './classpath/nodes/library-node'; +import { SourceNode } from './classpath/nodes/source-node'; import { ExternalLibrariesWidget, EXTERNAL_LIBRARIES_ID } from './libraries/external-libraries-widget'; import { createExternalLibrariesWidget } from './libraries/external-libraries-container'; @@ -64,4 +80,128 @@ export default new ContainerModule((bind, unbind, isBound) => { id: EXTERNAL_LIBRARIES_ID, createWidget: () => context.container.get(ExternalLibrariesWidget) })); + + /** + * Classpath configuration + */ + bind(MarkDirAsSourceAction).toSelf().inSingletonScope(); + bind(CommandContribution).toDynamicValue(ctx => ctx.container.get(MarkDirAsSourceAction)); + bind(MenuContribution).toDynamicValue(ctx => ctx.container.get(MarkDirAsSourceAction)); + + bind(UnmarkDirAsSourceAction).toSelf().inSingletonScope(); + bind(CommandContribution).toDynamicValue(ctx => ctx.container.get(UnmarkDirAsSourceAction)); + bind(MenuContribution).toDynamicValue(ctx => ctx.container.get(UnmarkDirAsSourceAction)); + + bind(ClassPathDialog).toSelf().inSingletonScope(); + bind(DialogProps).toConstantValue({ title: 'Configure Classpath' }); + + bind(ClasspathContainer).toSelf().inSingletonScope(); + + bind(IClasspathNode).to(LibraryNode).inSingletonScope(); + bind(IClasspathNode).to(SourceNode).inSingletonScope(); + + /** + * Build path tree widget + */ + bind(BuildPathTreeWidget).toDynamicValue(ctx => + createBuildPathTreeWidget(ctx.container) + ).inSingletonScope(); + + bind(WidgetFactory).toDynamicValue(context => ({ + id: BuildPathTreeWidgetID, + createWidget: () => context.container.get(BuildPathTreeWidget) + })); + + /** + * Library View widget + */ + bind(LibraryView).toDynamicValue(ctx => + createLibraryViewTreeWidget(ctx.container) + ).inSingletonScope(); + + bind(WidgetFactory).toDynamicValue(context => ({ + id: LibraryViewID, + createWidget: () => context.container.get(LibraryView) + })); + + /** + * Source View widget + */ + bind(SourceView).toDynamicValue(ctx => + createSourceViewTreeWidget(ctx.container) + ).inSingletonScope(); + + bind(WidgetFactory).toDynamicValue(context => ({ + id: SourceViewID, + createWidget: () => context.container.get(SourceView) + })); + + bind(ClasspathDecorator).toSelf().inSingletonScope(); + bind(NavigatorTreeDecorator).toService(ClasspathDecorator); + }); + +export const PROPS_PROPS = { + ...defaultTreeProps, + contextMenuPath: ["NAVIGATOR_CONTEXT_MENU"], + multiSelect: false +}; + +export function createBuildPathTreeWidgetContainer(parent: interfaces.Container): Container { + const child = createTreeContainer(parent); + + child.rebind(TreeProps).toConstantValue(PROPS_PROPS); + + child.unbind(TreeWidget); + child.bind(BuildPathTreeWidget).toSelf(); + + return child; +} + +export function createBuildPathTreeWidget(parent: interfaces.Container): BuildPathTreeWidget { + return createBuildPathTreeWidgetContainer(parent).get(BuildPathTreeWidget); +} + +/** + * Library view + */ +export function createLibraryViewTreeWidgetContainer(parent: interfaces.Container): Container { + const child = createTreeContainer(parent); + + child.rebind(TreeProps).toConstantValue(PROPS_PROPS); + + child.unbind(TreeModelImpl); + child.bind(LibraryModel).toSelf(); + child.rebind(TreeModel).toDynamicValue(ctx => ctx.container.get(LibraryModel)); + + child.unbind(TreeWidget); + child.bind(LibraryView).toSelf(); + + return child; +} + +export function createLibraryViewTreeWidget(parent: interfaces.Container): LibraryView { + return createLibraryViewTreeWidgetContainer(parent).get(LibraryView); +} + +/** + * Source view + */ +export function createSourceViewTreeWidgetContainer(parent: interfaces.Container): Container { + const child = createTreeContainer(parent); + + child.rebind(TreeProps).toConstantValue(PROPS_PROPS); + + child.unbind(TreeModelImpl); + child.bind(SourceModel).toSelf(); + child.rebind(TreeModel).toDynamicValue(ctx => ctx.container.get(SourceModel)); + + child.unbind(TreeWidget); + child.bind(SourceView).toSelf(); + + return child; +} + +export function createSourceViewTreeWidget(parent: interfaces.Container): SourceView { + return createSourceViewTreeWidgetContainer(parent).get(SourceView); +} diff --git a/che-theia-java-extension/src/browser/classpath/build-path-widget.tsx b/che-theia-java-extension/src/browser/classpath/build-path-widget.tsx new file mode 100644 index 0000000..d4ae44a --- /dev/null +++ b/che-theia-java-extension/src/browser/classpath/build-path-widget.tsx @@ -0,0 +1,148 @@ +/* + * Copyright (c) 2012-2018 Red Hat, Inc. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which is available at http://www.eclipse.org/legal/epl-2.0.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ + +import { injectable, inject, multiInject } from 'inversify'; +import { ContextMenuRenderer, TreeProps, TreeModel, TreeWidget, CompositeTreeNode, LabelProvider, Widget, WidgetManager } from '@theia/core/lib/browser'; +import { LanguageClientProvider } from '@theia/languages/lib/browser/language-client-provider'; +import { WorkspaceService } from '@theia/workspace/lib/browser'; +import { ClasspathContainer } from './classpath-container'; +import { IClasspathModel } from './pages/classpath-model'; +import { IClasspathNode } from './nodes/classpath-node'; +import { LibraryView } from './pages/library/library-view'; +import { FILE_NAVIGATOR_ID, FileNavigatorWidget } from '@theia/navigator/lib/browser/navigator-widget'; +import { AbstractClasspathTreeWidget } from './pages/classpath-tree-widget'; +import { JavaUtils } from '../java-utils'; +import { FileStat } from '@theia/filesystem/lib/common'; +import { ClasspathDialogRightPanelID } from './classpath-dialog'; + +export const BuildPathTreeWidgetID = 'Build path tree widget'; + +/** + * Left side of configure classpath that holds the libraries and the source node + */ +@injectable() +export class BuildPathTreeWidget extends TreeWidget { + + activeWidget: Widget | undefined; + activeClasspathURI: string | undefined; + + constructor( + @inject(TreeProps) readonly props: TreeProps, + @inject(TreeModel) readonly model: TreeModel, + @inject(ContextMenuRenderer) readonly contextMenuRenderer: ContextMenuRenderer, + @inject(LanguageClientProvider) protected readonly languageClientProvider: LanguageClientProvider, + @inject(WorkspaceService) protected readonly workspaceService: WorkspaceService, + @inject(ClasspathContainer) protected readonly classpathContainer: ClasspathContainer, + @inject(LabelProvider) protected readonly labelProvider: LabelProvider, + @multiInject(IClasspathNode) readonly classpathNodes: IClasspathNode[], + @inject(LibraryView) protected readonly libraryView: LibraryView, + @inject(WidgetManager) protected readonly widgetManager: WidgetManager + ) { + super(props, model, contextMenuRenderer); + this.addClass('classpath-widget'); + this.id = BuildPathTreeWidgetID; + this.model.onSelectionChanged(async e => { + const clickedNode = e[0] as IClasspathNode; + const rightPanel = document.getElementById(ClasspathDialogRightPanelID); + if (rightPanel) { + if (this.activeWidget) { + Widget.detach(this.activeWidget); + } + + Widget.attach(clickedNode.widget, rightPanel); + clickedNode.widget.update(); + this.activeWidget = clickedNode.widget; + this.update(); + } + }); + } + + async createBuildPathTree() { + const rootNode = { + id: 'build-path-root', + name: 'Java build path', + visible: true, + parent: undefined + } as CompositeTreeNode; + rootNode.children = await this.createBuildPathTreeChildren(rootNode); + this.model.root = rootNode; + } + + async createBuildPathTreeChildren(parent: Readonly): Promise { + let activeFileStat = await this.getActiveClasspathFileStat(); + if (activeFileStat) { + this.activeClasspathURI = activeFileStat.uri; + + const classpathNodes = await this.classpathContainer.getClassPathEntries(activeFileStat.uri); + this.classpathContainer.resolveClasspathEntries(classpathNodes); + for (const classpathNode of this.classpathNodes) { + const classpathWidget = classpathNode.widget as AbstractClasspathTreeWidget; + classpathWidget.activeFileStat = activeFileStat; + const c = classpathWidget.model as IClasspathModel; + c.addClasspathNodes(classpathNodes); + } + + this.classpathContainer.onClasspathModelChangeEmitter.fire({ + classpathItems: classpathNodes, + uri: activeFileStat.uri + }); + return this.classpathNodes; + } + return []; + } + + /** + * Get the classpath file stat for the active configure classpath session + */ + async getActiveClasspathFileStat(): Promise { + const roots = await this.workspaceService.roots; + const fileModel = await this.widgetManager.getWidget(FILE_NAVIGATOR_ID) as FileNavigatorWidget; + if (roots && fileModel) { + const selectedNodes = fileModel.model.selectedFileStatNodes; + const classpathURI = selectedNodes.length > 0 ? selectedNodes[0].fileStat : roots[0]; + const classpathURI2 = JavaUtils.getRootProjectURI(roots, classpathURI.uri.toString()) as string; + classpathURI.uri = classpathURI2; + return classpathURI; + } + return undefined; + } + + isDirty(): boolean { + for (const c of this.classpathNodes) { + const model = c.widget.model as IClasspathModel; + if (model.isDirty) { + return true; + } + } + return false; + } + + async save() { + if (this.activeClasspathURI) { + this.classpathContainer.updateClasspath(this.activeClasspathURI); + } + this.resetState(); + } + + /** + * Called when the dialog is closed and we reset the model and items + */ + resetState(): void { + for (const c of this.classpathNodes) { + const model = c.widget.model as IClasspathModel; + model.isDirty = false; + model.currentClasspathItems.clear(); + } + this.classpathContainer.clearClasspathEntries(); + } + +} diff --git a/che-theia-java-extension/src/browser/classpath/classpath-container.ts b/che-theia-java-extension/src/browser/classpath/classpath-container.ts new file mode 100644 index 0000000..7f13768 --- /dev/null +++ b/che-theia-java-extension/src/browser/classpath/classpath-container.ts @@ -0,0 +1,178 @@ +/* + * Copyright (c) 2012-2018 Red Hat, Inc. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which is available at http://www.eclipse.org/legal/epl-2.0.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ + +import { injectable, inject } from 'inversify'; +import { LanguageClientProvider } from '@theia/languages/lib/browser/language-client-provider'; +import { ExecuteCommandRequest } from '@theia/languages/lib/browser'; +import { UPDATE_PROJECT_CLASSPATH, GET_CLASS_PATH_TREE_COMMAND } from '../che-ls-jdt-commands'; +import { Event, Emitter } from '@theia/core/lib/common'; + +export interface ClasspathEntry { + entryKind: ClasspathEntryKind, + path: string, + children: ClasspathEntry[] +} + +export enum ClasspathEntryKind { + LIBRARY = 1, + PROJECT = 2, + SOURCE = 3, + VARIABLE = 4, + CONTAINER = 5 +} + +export interface ClasspathChangeNotification { + classpathItems: ClasspathEntry[]; + uri: string; +} + +/** + * Holds all the current classpath items for a project and updates items to che-ls-jdts + */ +@injectable() +export class ClasspathContainer { + + private static WORKSPACE_PATH = "/projects"; + + private classpath = new Map>(); + + private libs = new Set(); + private containers = new Set(); + private sources = new Set(); + private projects = new Set(); + + readonly onClasspathModelChangeEmitter: Emitter = new Emitter(); + public onClasspathModelChange: Event = this.onClasspathModelChangeEmitter.event; + + constructor(@inject(LanguageClientProvider) protected readonly languageClientProvider: LanguageClientProvider) { + } + + /** + * Reads and parses classpath entries + */ + resolveClasspathEntries(entries: ClasspathEntry[]): void { + for (const entry of entries) { + switch (entry.entryKind) { + case ClasspathEntryKind.LIBRARY: + this.libs.add(entry.path); + break; + case ClasspathEntryKind.CONTAINER: + this.containers.add(entry); + break; + case ClasspathEntryKind.SOURCE: + this.sources.add(entry.path); + break; + case ClasspathEntryKind.PROJECT: + this.projects.add(ClasspathContainer.WORKSPACE_PATH + entry.path); + break; + default: + } + } + } + + clearClasspathEntries() { + this.libs.clear(); + this.containers.clear(); + this.sources.clear(); + this.projects.clear(); + } + + removeClasspathEntry(entry: ClasspathEntry): void { + switch (entry.entryKind) { + case ClasspathEntryKind.LIBRARY: + this.libs.delete(entry.path); + break; + case ClasspathEntryKind.CONTAINER: + this.containers.delete(entry); + break; + case ClasspathEntryKind.SOURCE: + this.sources.delete(entry.path); + break; + case ClasspathEntryKind.PROJECT: + this.projects.delete(ClasspathContainer.WORKSPACE_PATH + entry.path); + break; + } + } + + async updateClasspath(projectURI: string) { + const classpathEntries: ClasspathEntry[] = []; + + this.libs.forEach(path => classpathEntries.push({ + path, + entryKind: ClasspathEntryKind.LIBRARY + } as ClasspathEntry)); + + this.containers.forEach(entry => classpathEntries.push(entry)); + + this.sources.forEach(path => classpathEntries.push({ + path, + entryKind: ClasspathEntryKind.SOURCE + } as ClasspathEntry)); + + this.projects.forEach(path => classpathEntries.push({ + path, + entryKind: ClasspathEntryKind.PROJECT + } as ClasspathEntry)); + + this.classpath.set(projectURI, Promise.resolve(classpathEntries)); + //Classpath updater set raw classpath + this.update(projectURI, classpathEntries); + this.onClasspathModelChangeEmitter.fire({ + uri: projectURI, + classpathItems: classpathEntries + }); + } + + private async update(projectURI: string, classpathEntries: ClasspathEntry[]) { + const javaClient = await this.languageClientProvider.getLanguageClient("java"); + if (javaClient) { + javaClient.sendRequest(ExecuteCommandRequest.type, { + command: UPDATE_PROJECT_CLASSPATH, + arguments: [ + { + uri: projectURI, + entries: classpathEntries + } + ] + }); + this.clearClasspathEntries(); + } + } + + /** + * Returns list of classpath entries. If the classpath exists + * for the project path return otherwise get the classpath from server + */ + async getClassPathEntries(projectPath: string): Promise { + if (this.classpath.has(projectPath)) { + return this.classpath.get(projectPath) || []; + } else { + const javaClient = await this.languageClientProvider.getLanguageClient("java"); + if (javaClient) { + const result = await javaClient.sendRequest(ExecuteCommandRequest.type, { + command: GET_CLASS_PATH_TREE_COMMAND, + arguments: [ + projectPath + ] + }); + this.classpath.set(projectPath, result); + return result; + } + return []; + } + } + + getClasspathItems(projectPath: string) { + return this.classpath.get(projectPath) || []; + } + +} diff --git a/che-theia-java-extension/src/browser/classpath/classpath-dialog.ts b/che-theia-java-extension/src/browser/classpath/classpath-dialog.ts new file mode 100644 index 0000000..e69e47a --- /dev/null +++ b/che-theia-java-extension/src/browser/classpath/classpath-dialog.ts @@ -0,0 +1,115 @@ +/* + * Copyright (c) 2012-2018 Red Hat, Inc. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which is available at http://www.eclipse.org/legal/epl-2.0.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ + +import { injectable, inject } from "inversify"; +import { AbstractDialog, Message, Widget, LabelProvider, SelectableTreeNode, ConfirmDialog } from "@theia/core/lib/browser"; +import { Disposable } from "@theia/core"; +import { BuildPathTreeWidget } from "./build-path-widget"; +import { WorkspaceService } from "@theia/workspace/lib/browser"; +import { LibraryView } from "./pages/library/library-view"; +import { LibraryNodeID } from "./nodes/library-node"; + +@injectable() +export class DialogProps { + readonly title: string = "Configure Classpath"; +} + +export const ClasspathDialogRightPanelID = 'classpath-panel-right'; + +@injectable() +export abstract class ClassPathDialog extends AbstractDialog { + + private leftPanel: HTMLElement; + rightPanel: HTMLElement; + + constructor(@inject(DialogProps) protected readonly props: DialogProps, + @inject(BuildPathTreeWidget) protected readonly buildPathTreeWidget: BuildPathTreeWidget, + @inject(WorkspaceService) protected readonly workspaceService: WorkspaceService, + @inject(LabelProvider) protected readonly labelProvider: LabelProvider, + @inject(LibraryView) protected readonly libraryView: LibraryView) { + super(props); + + if (this.contentNode.parentElement) { + this.contentNode.parentElement.classList.add('classpath-modal'); + } + + this.leftPanel = document.createElement('div'); + this.leftPanel.classList.add('classpath-panel'); + this.leftPanel.classList.add('classpath-panel-left'); + + this.rightPanel = document.createElement('div'); + this.rightPanel.classList.add('classpath-panel'); + this.rightPanel.classList.add('classpath-panel-right'); + this.rightPanel.id = ClasspathDialogRightPanelID; + + this.contentNode.classList.remove('dialogContent'); + this.contentNode.classList.add('classpath-content'); + + this.contentNode.appendChild(this.leftPanel); + this.contentNode.appendChild(this.rightPanel); + + const button = this.createButton('Done'); + button.classList.add('classpath-button-done'); + button.onclick = () => { + this.buildPathTreeWidget.save(); + this.close(); + }; + this.controlPanel.appendChild(button); + + this.closeCrossNode.onclick = async () => { + if (this.buildPathTreeWidget.isDirty()) { + const dialog = new ConfirmDialog({ + title: `The classpath has been modified`, + msg: 'Do you want to overwrite the classpath changes?', + ok: 'Yes', + cancel: 'No' + }); + await dialog.open() ? this.buildPathTreeWidget.save() : this.buildPathTreeWidget.resetState(); + } else { + this.close(); + } + }; + } + + protected onAfterAttach(msg: Message): void { + super.onAfterAttach(msg); + Widget.attach(this.buildPathTreeWidget, this.leftPanel); + this.toDisposeOnDetach.push(Disposable.create(() => { + Widget.detach(this.buildPathTreeWidget); + if (this.buildPathTreeWidget.activeWidget) { + Widget.detach(this.buildPathTreeWidget.activeWidget); + this.resetSelectedItems(); + } + })); + } + + private resetSelectedItems() { + for (const classpathNode of this.buildPathTreeWidget.classpathNodes) { + classpathNode.selected = false; + } + const libNode = this.buildPathTreeWidget.model.getNode(LibraryNodeID) as SelectableTreeNode; + libNode.selected = true; + } + + protected onUpdateRequest(msg: Message): void { + super.onUpdateRequest(msg); + this.buildPathTreeWidget.update(); + } + + protected onActivateRequest(msg: Message): void { + super.onActivateRequest(msg); + this.buildPathTreeWidget.createBuildPathTree(); + Widget.attach(this.libraryView, this.rightPanel); + this.buildPathTreeWidget.activeWidget = this.libraryView; + } + +} diff --git a/che-theia-java-extension/src/browser/classpath/classpath-tree-decorator.ts b/che-theia-java-extension/src/browser/classpath/classpath-tree-decorator.ts new file mode 100644 index 0000000..8b8b083 --- /dev/null +++ b/che-theia-java-extension/src/browser/classpath/classpath-tree-decorator.ts @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2012-2018 Red Hat, Inc. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which is available at http://www.eclipse.org/legal/epl-2.0.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ + +import { TreeDecorator, TreeDecoration } from '@theia/core/lib/browser/tree/tree-decorator'; +import { Tree, WidgetManager, TreeNode, LabelProvider } from '@theia/core/lib/browser'; +import { Event, Emitter } from '@theia/core/lib/common/event'; +import { injectable, inject } from 'inversify'; +import { ClasspathContainer, ClasspathEntry, ClasspathEntryKind } from './classpath-container'; +import { WorkspaceService } from '@theia/workspace/lib/browser'; +import { JavaUtils } from '../java-utils'; + +@injectable() +export class ClasspathDecorator implements TreeDecorator { + + id: string = "classpath-decorator"; + + protected readonly emitter: Emitter<(tree: Tree) => Map>; + currentlyDecorated: Set = new Set(); + + constructor(@inject(ClasspathContainer) protected readonly classpathContainer: ClasspathContainer, + @inject(WorkspaceService) protected readonly workspaceService: WorkspaceService, + @inject(WidgetManager) protected readonly widgetManager: WidgetManager, + @inject(LabelProvider) protected readonly labelProvider: LabelProvider) { + this.emitter = new Emitter(); + this.classpathContainer.onClasspathModelChange(c => { + this.fireDidChangeDecorations((tree: Tree) => this.collectDecorators(tree, c.classpathItems, c.uri)); + }); + } + + async decorations(tree: Tree): Promise> { + const roots = await this.workspaceService.roots; + if (roots) { + const toDecorate = new Map(); + for (const root of roots) { + const classpathItems = await this.classpathContainer.getClassPathEntries(root.uri); + const rootDecorations = this.collectDecorators(tree, classpathItems, root.uri); + rootDecorations.forEach((decorator, path) => { + toDecorate.set(path, decorator); + }); + } + return toDecorate; + } + return new Map(); + } + + protected toDecorator(): TreeDecoration.Data { + const position = TreeDecoration.IconOverlayPosition.BOTTOM_LEFT; + const icon = 'fa fa-times-circle'; + const color = 'var(--theia-brand-color1)'; + return { + iconOverlay: { + position, + icon, + color + } + }; + } + + protected collectDecorators(tree: Tree, classpathItems: ClasspathEntry[], uri: string): Map { + let toDecorate = new Map(); + for (const classpathItem of classpathItems) { + if (classpathItem.entryKind !== ClasspathEntryKind.SOURCE) { + continue; + } + + const multiRootURI = JavaUtils.getMultiRootReadyURI(uri, classpathItem.path); + let navigatorTreeNode = tree.getNode(multiRootURI); + if (navigatorTreeNode) { + toDecorate.set(navigatorTreeNode.id, this.toDecorator()); + } + } + return toDecorate; + } + + get onDidChangeDecorations(): Event<(tree: Tree) => Map> { + return this.emitter.event; + } + + protected fireDidChangeDecorations(event: (tree: Tree) => Map): void { + this.emitter.fire(event); + } + +} \ No newline at end of file diff --git a/che-theia-java-extension/src/browser/classpath/nodes/classpath-node.ts b/che-theia-java-extension/src/browser/classpath/nodes/classpath-node.ts new file mode 100644 index 0000000..455136a --- /dev/null +++ b/che-theia-java-extension/src/browser/classpath/nodes/classpath-node.ts @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2012-2018 Red Hat, Inc. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which is available at http://www.eclipse.org/legal/epl-2.0.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ + +import { SelectableTreeNode, TreeNode, TreeWidget } from "@theia/core/lib/browser"; +import { ClasspathEntry } from "../classpath-container"; + +export const IClasspathNode = Symbol('IClasspathNode'); + +export interface IClasspathNode extends SelectableTreeNode { + widget: TreeWidget; +} + +export interface ClasspathViewNode extends TreeNode { + classpathEntry: ClasspathEntry; + children?: ClasspathViewNode[], + expanded?: boolean +} diff --git a/che-theia-java-extension/src/browser/classpath/nodes/library-node.ts b/che-theia-java-extension/src/browser/classpath/nodes/library-node.ts new file mode 100644 index 0000000..7fb811e --- /dev/null +++ b/che-theia-java-extension/src/browser/classpath/nodes/library-node.ts @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2012-2018 Red Hat, Inc. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which is available at http://www.eclipse.org/legal/epl-2.0.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ + +import { injectable, inject } from "inversify"; +import { CompositeTreeNode, TreeWidget } from "@theia/core/lib/browser"; +import { LibraryView } from "../pages/library/library-view"; +import { IClasspathNode } from "./classpath-node"; + +export const LibraryNodeID = "Library node"; + +@injectable() +export class LibraryNode implements IClasspathNode { + + selected: boolean; + widget: TreeWidget; + id: string; + name: string; + parent: CompositeTreeNode | undefined; + + constructor(@inject(LibraryView) protected readonly libraryView: LibraryView) { + this.selected = false; + this.id = LibraryNodeID; + this.name = this.id; + this.widget = libraryView; + } + +} \ No newline at end of file diff --git a/che-theia-java-extension/src/browser/classpath/nodes/source-node.ts b/che-theia-java-extension/src/browser/classpath/nodes/source-node.ts new file mode 100644 index 0000000..bb69476 --- /dev/null +++ b/che-theia-java-extension/src/browser/classpath/nodes/source-node.ts @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2012-2018 Red Hat, Inc. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which is available at http://www.eclipse.org/legal/epl-2.0.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ + +import { injectable, inject } from 'inversify'; +import { CompositeTreeNode, TreeWidget } from '@theia/core/lib/browser'; +import { SourceView } from '../pages/source/source-view'; +import { IClasspathNode } from './classpath-node'; + +export const SourceNodeID = 'Source node'; + +@injectable() +export class SourceNode implements IClasspathNode { + + selected: boolean; + widget: TreeWidget; + id: string; + name: string; + parent: CompositeTreeNode | undefined; + + constructor(@inject(SourceView) protected readonly sourceView: SourceView) { + this.selected = false; + this.id = SourceNodeID; + this.name = this.id; + this.widget = sourceView; + } + +} \ No newline at end of file diff --git a/che-theia-java-extension/src/browser/classpath/pages/classpath-model.ts b/che-theia-java-extension/src/browser/classpath/pages/classpath-model.ts new file mode 100644 index 0000000..2a32e40 --- /dev/null +++ b/che-theia-java-extension/src/browser/classpath/pages/classpath-model.ts @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2012-2018 Red Hat, Inc. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which is available at http://www.eclipse.org/legal/epl-2.0.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ + +import { ClasspathEntry } from "../classpath-container"; +import { TreeModelImpl, CompositeTreeNode } from "@theia/core/lib/browser"; +import { ClasspathViewNode } from "../nodes/classpath-node"; + +export const IClasspathModel = Symbol('IClasspathModel'); + +export interface IClasspathModel extends TreeModelImpl { + currentClasspathItems: Map; + addClasspathNodes(classpathItems: ClasspathEntry[] | ClasspathEntry): void; + removeClasspathNode(path: string): void; + isDirty: boolean; + updateTree(): void; +} + +export const ClasspathRootID = 'class-path-root'; +export const ClasspathRootName = 'java-class-path-root'; + +export abstract class AbstractClasspathModel extends TreeModelImpl implements IClasspathModel { + + currentClasspathItems: Map; + isDirty = false; + + constructor() { + super(); + this.currentClasspathItems = new Map(); + } + + addClasspathNodes(classpathItems: ClasspathEntry | ClasspathEntry[]): void { + throw new Error('Method not implemented.'); + } + + removeClasspathNode(path: string): void { + this.isDirty = true; + this.currentClasspathItems.delete(path); + this.updateTree(); + } + + get classpathItems(): ClasspathViewNode[] { + return Array.from(this.currentClasspathItems.values()); + } + + updateTree() { + const rootNode = { + id: ClasspathRootID, + name: ClasspathRootName, + visible: false, + parent: undefined, + children: this.classpathItems + } as CompositeTreeNode; + this.root = rootNode; + } + +} \ No newline at end of file diff --git a/che-theia-java-extension/src/browser/classpath/pages/classpath-tree-widget.tsx b/che-theia-java-extension/src/browser/classpath/pages/classpath-tree-widget.tsx new file mode 100644 index 0000000..8be31c2 --- /dev/null +++ b/che-theia-java-extension/src/browser/classpath/pages/classpath-tree-widget.tsx @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2012-2018 Red Hat, Inc. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which is available at http://www.eclipse.org/legal/epl-2.0.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ + +import { injectable, inject } from 'inversify'; +import { ContextMenuRenderer, TreeProps, LabelProvider, TreeWidget, TreeNode, NodeProps, TreeModel } from '@theia/core/lib/browser'; +import { LanguageClientProvider } from '@theia/languages/lib/browser/language-client-provider'; +import { WorkspaceService } from '@theia/workspace/lib/browser'; +import * as React from 'react'; +import { DirNode, OpenFileDialogFactory, OpenFileDialogProps } from '@theia/filesystem/lib/browser'; +import { IClasspathModel, ClasspathRootID } from './classpath-model'; +import { ClasspathContainer, ClasspathEntry, ClasspathEntryKind } from '../classpath-container'; +import { FileStat } from '@theia/filesystem/lib/common'; +import { ClasspathViewNode } from '../nodes/classpath-node'; + +export interface ExtendedDialogProps { + entryKindOnAdded: ClasspathEntryKind +} + +/** + * This is the left side of the panel that holds the libraries and the source node + */ +@injectable() +export abstract class AbstractClasspathTreeWidget extends TreeWidget { + + classpathModel: IClasspathModel; + activeFileStat: FileStat | undefined; + fileDialogProps!: OpenFileDialogProps & ExtendedDialogProps; + + constructor( + @inject(TreeProps) readonly props: TreeProps, + @inject(IClasspathModel) classpathModel: IClasspathModel, + @inject(ContextMenuRenderer) readonly contextMenuRenderer: ContextMenuRenderer, + @inject(LanguageClientProvider) protected readonly languageClientProvider: LanguageClientProvider, + @inject(WorkspaceService) protected readonly workspaceService: WorkspaceService, + @inject(ClasspathContainer) protected readonly classpathContainer: ClasspathContainer, + @inject(LabelProvider) protected readonly labelProvider: LabelProvider, + @inject(OpenFileDialogFactory) protected readonly openFileDialogFactory: OpenFileDialogFactory + ) { + super(props, classpathModel, contextMenuRenderer); + this.addClass('classpath-widget'); + this.classpathModel = classpathModel; + this.classpathModel.updateTree(); + } + + protected renderTree(model: TreeModel): React.ReactNode { + if (model.root) { + return this.view = (view || undefined)} + width={this.node.offsetWidth * 0.8} + height={this.node.offsetHeight * 0.8} + rows={Array.from(this.rows.values())} + renderNodeRow={this.renderNodeRow} + scrollToRow={this.scrollToRow} + handleScroll={this.handleScroll} + />; + } + return null; + } + + protected renderIcon(node: TreeNode, props: NodeProps): React.ReactNode { + return
; + } + + protected renderTailDecorations(node: TreeNode, props: NodeProps): React.ReactNode { + const c = node as ClasspathViewNode; + if ((c.parent && c.parent.id === ClasspathRootID && c.classpathEntry.entryKind === ClasspathEntryKind.LIBRARY) || + c.classpathEntry.entryKind === ClasspathEntryKind.SOURCE) { + return
this.removeNode(node)}>
; + } + return super.renderTailDecorations(node, props); + } + + protected removeNode(node: TreeNode) { + const classpathViewNode = node as ClasspathViewNode; + this.classpathModel.removeClasspathNode(classpathViewNode.classpathEntry.path); + this.classpathContainer.removeClasspathEntry(classpathViewNode.classpathEntry); + } + + async openDialog() { + if (this.activeFileStat) { + const rootNode = DirNode.createRoot(this.activeFileStat, this.labelProvider.getName(this.activeFileStat), this.activeFileStat.uri); + const title = this.fileDialogProps.title; + const dialog = this.openFileDialogFactory(Object.assign(this.fileDialogProps, { title })); + dialog.model.navigateTo(rootNode); + const result = await dialog.open(); + + if (result && !Array.isArray(result) && this.isValidOpenedNode(result)) { + const newClasspathItem = { + entryKind: this.fileDialogProps.entryKindOnAdded, + path: result.fileStat.uri + } as ClasspathEntry; + this.classpathModel.addClasspathNodes(newClasspathItem); + this.classpathContainer.resolveClasspathEntries([newClasspathItem]); + this.update(); + } + } + } + + protected isValidOpenedNode(node: TreeNode): boolean { + return false; + } +} + +export namespace ClasspathTreeWidget { + export namespace Styles { + export const CLASSPATHTREEWIDGET_STYLE_ICONS = 'file-icon java-libraries-icon'; + export const CLASSPATHTREEWIDGET_REMOVE_ICON = 'java-remove-node-icon file-icon java-libraries-icon'; + } +} diff --git a/che-theia-java-extension/src/browser/classpath/pages/library/library-model.ts b/che-theia-java-extension/src/browser/classpath/pages/library/library-model.ts new file mode 100644 index 0000000..975e07b --- /dev/null +++ b/che-theia-java-extension/src/browser/classpath/pages/library/library-model.ts @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2012-2018 Red Hat, Inc. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which is available at http://www.eclipse.org/legal/epl-2.0.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ + +import { ClasspathEntry, ClasspathEntryKind } from "../../classpath-container"; +import { LabelProvider } from "@theia/core/lib/browser"; +import URI from "@theia/core/lib/common/uri"; +import { injectable, inject } from "inversify"; +import { AbstractClasspathModel } from "../classpath-model"; +import { ClasspathViewNode } from "../../nodes/classpath-node"; + +@injectable() +export class LibraryModel extends AbstractClasspathModel { + + constructor(@inject(LabelProvider) protected readonly labelProvider: LabelProvider) { + super(); + } + + addClasspathNodes(classpathEntry: ClasspathEntry[] | ClasspathEntry) { + if (Array.isArray(classpathEntry)) { + for (const result of classpathEntry) { + if (result.entryKind !== ClasspathEntryKind.CONTAINER && result.entryKind !== ClasspathEntryKind.LIBRARY) { + continue; + } + + const classpathNode = this.createClasspathNodes(result); + this.currentClasspathItems.set(result.path, classpathNode); + } + } else { + this.isDirty = true; + if (classpathEntry.entryKind === ClasspathEntryKind.CONTAINER || classpathEntry.entryKind === ClasspathEntryKind.LIBRARY) { + const classpathNode = this.createClasspathNodes(classpathEntry); + this.currentClasspathItems.set(classpathEntry.path, classpathNode); + } + } + this.updateTree(); + } + + private createClasspathNodes(result: ClasspathEntry) { + let childNodes = []; + if (result.children) { + for (const child of result.children) { + const childNode = { + id: child.path, + name: this.labelProvider.getName(new URI(child.path)) + " - " + this.labelProvider.getLongName(new URI(child.path)), + icon: "java-jar-icon", + classpathEntry: child + } as ClasspathViewNode; + childNodes.push(childNode); + } + } + + const resultNode = { + id: result.path, + name: this.labelProvider.getName(new URI(result.path)), + icon: "java-externalLibraries-icon", + parent: undefined, + classpathEntry: result + } as ClasspathViewNode; + + if (childNodes.length > 0) { + resultNode.expanded = false; + resultNode.children = childNodes; + } + + return resultNode; + } + +} \ No newline at end of file diff --git a/che-theia-java-extension/src/browser/classpath/pages/library/library-view.tsx b/che-theia-java-extension/src/browser/classpath/pages/library/library-view.tsx new file mode 100644 index 0000000..14a99e6 --- /dev/null +++ b/che-theia-java-extension/src/browser/classpath/pages/library/library-view.tsx @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2012-2018 Red Hat, Inc. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which is available at http://www.eclipse.org/legal/epl-2.0.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ + +import { injectable, inject } from 'inversify'; +import { ContextMenuRenderer, TreeProps, LabelProvider, TreeNode } from '@theia/core/lib/browser'; +import { LanguageClientProvider } from '@theia/languages/lib/browser/language-client-provider'; +import { WorkspaceService } from '@theia/workspace/lib/browser'; +import * as React from 'react'; +import { OpenFileDialogFactory } from '@theia/filesystem/lib/browser'; +import { ClasspathContainer, ClasspathEntryKind } from '../../classpath-container'; +import { LibraryModel } from './library-model'; +import { AbstractClasspathTreeWidget } from '../classpath-tree-widget'; + +export const LibraryViewID = 'Library View'; + +@injectable() +export class LibraryView extends AbstractClasspathTreeWidget { + + classpathModel: LibraryModel; + + constructor( + @inject(TreeProps) readonly props: TreeProps, + @inject(LibraryModel) classpathModel: LibraryModel, + @inject(ContextMenuRenderer) readonly contextMenuRenderer: ContextMenuRenderer, + @inject(LanguageClientProvider) protected readonly languageClientProvider: LanguageClientProvider, + @inject(WorkspaceService) protected readonly workspaceService: WorkspaceService, + @inject(ClasspathContainer) protected readonly classpathContainer: ClasspathContainer, + @inject(LabelProvider) protected readonly labelProvider: LabelProvider, + @inject(OpenFileDialogFactory) protected readonly openFileDialogFactory: OpenFileDialogFactory + ) { + super(props, classpathModel, contextMenuRenderer, languageClientProvider, workspaceService, classpathContainer, labelProvider, openFileDialogFactory); + this.classpathModel = classpathModel; + this.id = LibraryViewID; + this.addClass('library-widget'); + this.fileDialogProps = { + canSelectFiles: true, + canSelectFolders: false, + canSelectMany: false, + title: "Add a jar", + entryKindOnAdded: ClasspathEntryKind.LIBRARY + } + } + + protected render(): React.ReactNode { + const tree = this.renderTree(this.classpathModel); + return ( +
+
+

JARs and class folders on the build path

+ { tree } +
+
+ +
+
+ ); + } + + protected isValidOpenedNode(node: TreeNode): boolean { + return node.id.endsWith('.jar'); + } + +} diff --git a/che-theia-java-extension/src/browser/classpath/pages/source/source-model.ts b/che-theia-java-extension/src/browser/classpath/pages/source/source-model.ts new file mode 100644 index 0000000..7372ccd --- /dev/null +++ b/che-theia-java-extension/src/browser/classpath/pages/source/source-model.ts @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2012-2018 Red Hat, Inc. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which is available at http://www.eclipse.org/legal/epl-2.0.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ + +import { ClasspathEntry, ClasspathEntryKind } from "../../classpath-container"; +import { LabelProvider } from "@theia/core/lib/browser"; +import URI from "@theia/core/lib/common/uri"; +import { injectable, inject } from "inversify"; +import { AbstractClasspathModel } from "../classpath-model"; +import { ClasspathViewNode } from "../../nodes/classpath-node"; + + @injectable() + export class SourceModel extends AbstractClasspathModel { + + constructor(@inject(LabelProvider) protected readonly labelProvider: LabelProvider) { + super(); + } + + addClasspathNodes(classpathEntry: ClasspathEntry[] | ClasspathEntry) { + if (Array.isArray(classpathEntry)) { + for (const result of classpathEntry) { + + if (result.entryKind !== ClasspathEntryKind.SOURCE) { + continue; + } + + const classpathViewNode = this.createClasspathNode(result); + this.currentClasspathItems.set(result.path, classpathViewNode); + } + } else { + this.isDirty = true; + if (classpathEntry.entryKind === ClasspathEntryKind.SOURCE) { + const classpathViewNode = this.createClasspathNode(classpathEntry); + this.currentClasspathItems.set(classpathEntry.path, classpathViewNode); + } + } + this.updateTree(); + } + + createClasspathNode(result: ClasspathEntry) { + const resultNode = { + id: result.path, + name: this.labelProvider.getLongName(new URI(result.path)), + icon: "java-source-folder-icon", + parent: undefined, + classpathEntry: result, + isRemoveable: true + } as ClasspathViewNode; + + return resultNode; + } + + } diff --git a/che-theia-java-extension/src/browser/classpath/pages/source/source-view.tsx b/che-theia-java-extension/src/browser/classpath/pages/source/source-view.tsx new file mode 100644 index 0000000..793bb12 --- /dev/null +++ b/che-theia-java-extension/src/browser/classpath/pages/source/source-view.tsx @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2012-2018 Red Hat, Inc. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which is available at http://www.eclipse.org/legal/epl-2.0.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ + +import { injectable, inject } from 'inversify'; +import { ContextMenuRenderer, TreeProps, LabelProvider, TreeNode, CompositeTreeNode } from '@theia/core/lib/browser'; +import { LanguageClientProvider } from '@theia/languages/lib/browser/language-client-provider'; +import { WorkspaceService } from '@theia/workspace/lib/browser'; +import * as React from 'react'; +import { FileDialogService, OpenFileDialogFactory } from '@theia/filesystem/lib/browser'; +import { ClasspathContainer, ClasspathEntryKind } from '../../classpath-container'; +import { SourceModel } from './source-model'; +import { AbstractClasspathTreeWidget } from '../classpath-tree-widget'; + +export const SourceViewID = 'Source View Widget'; + +@injectable() +export class SourceView extends AbstractClasspathTreeWidget { + + classpathModel: SourceModel; + + constructor( + @inject(TreeProps) readonly props: TreeProps, + @inject(SourceModel) classpathModel: SourceModel, + @inject(ContextMenuRenderer) readonly contextMenuRenderer: ContextMenuRenderer, + @inject(LanguageClientProvider) protected readonly languageClientProvider: LanguageClientProvider, + @inject(WorkspaceService) protected readonly workspaceService: WorkspaceService, + @inject(ClasspathContainer) protected readonly classpathContainer: ClasspathContainer, + @inject(LabelProvider) protected readonly labelProvider: LabelProvider, + @inject(FileDialogService) protected readonly fileDialogService: FileDialogService, + @inject(OpenFileDialogFactory) protected readonly openFileDialogFactory: OpenFileDialogFactory + ) { + super(props, classpathModel, contextMenuRenderer, languageClientProvider, workspaceService, classpathContainer, labelProvider, openFileDialogFactory); + this.addClass('classpath-widget'); + this.addClass('source-widget'); + this.id = SourceViewID; + this.classpathModel = classpathModel; + this.fileDialogProps = { + canSelectFiles: false, + canSelectFolders: true, + canSelectMany: false, + title: "Add a source folder", + entryKindOnAdded: ClasspathEntryKind.SOURCE + } + } + + protected render(): React.ReactNode { + const tree = this.renderTree(this.classpathModel); + return ( +
+
+

Source folders on build path

+ { tree } +
+
+ +
+
+ ); + } + + protected isValidOpenedNode(node: TreeNode): boolean { + return (node as CompositeTreeNode).children !== undefined; + } + +} diff --git a/che-theia-java-extension/src/browser/java-utils.ts b/che-theia-java-extension/src/browser/java-utils.ts new file mode 100644 index 0000000..6d63af4 --- /dev/null +++ b/che-theia-java-extension/src/browser/java-utils.ts @@ -0,0 +1,36 @@ +import { FileStat } from "@theia/filesystem/lib/common"; + + +export class JavaUtils { + + static FILE = "file://"; + + static getRootProjectURI(roots: FileStat[], path: string): string | undefined { + if (roots) { + for (const root of roots) { + if (path.includes(root.uri)) { + return root.uri; + } + } + } + return undefined; + } + + static uriToTreeNodeID(uri: string) { + return uri.replace(this.FILE, ""); + } + + static getIDFromMultiRootID(multiRootID: string) { + const fixed = multiRootID.split(":"); + const uri = fixed[1]; + return JavaUtils.FILE + uri; + } + + static getMultiRootReadyURI(root: string, treeNodePath: string) { + const fixedRoot = root.replace(JavaUtils.FILE, ""); + const fixedTreeNodePath = treeNodePath.replace(JavaUtils.FILE, ""); + return fixedRoot + ":" + fixedTreeNodePath; + } + + +} \ No newline at end of file diff --git a/che-theia-java-extension/src/browser/styles/classpath.css b/che-theia-java-extension/src/browser/styles/classpath.css new file mode 100644 index 0000000..0d5ccfc --- /dev/null +++ b/che-theia-java-extension/src/browser/styles/classpath.css @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2012-2018 Red Hat, Inc. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which is available at http://www.eclipse.org/legal/epl-2.0.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ + +.classpath-panel { + padding-top: calc(var(--theia-ui-padding)); +} + +.classpath-panel-left { + flex: 1; + padding-right: calc(var(--theia-ui-padding)); +} + +.classpath-panel-right { + flex: 2; +} + +.classpath-modal { + max-width: 650px; + width: 650px; +} + +.classpath-content { + height: 420px; + overflow: hidden; + display: flex; +} + +.classpath-widget { + height: 100%; +} + +.classpath-button-right { + float: right; + width: 15%; + padding-top: 50px; + padding-right: 20px; +} + +.classpath-tree-left { + width: 80%; + float: left; + height: 410px; +} + +.classpath-button-done { + margin-top: calc(var(--theia-ui-padding)*2); +} + +.classpath-view-title { + margin-left: calc(var(--theia-ui-padding)*3); +} + +.source-widget .theia-TreeNode:hover .java-remove-node-icon, .library-widget .theia-TreeNode:hover .java-remove-node-icon { + display: block; +} + +.dialogOverlay .classpath-widget :focus { + border: none; + box-shadow: none; + outline: none; +} + diff --git a/che-theia-java-extension/src/browser/styles/icons.css b/che-theia-java-extension/src/browser/styles/icons.css index af4ab65..da7f37b 100644 --- a/che-theia-java-extension/src/browser/styles/icons.css +++ b/che-theia-java-extension/src/browser/styles/icons.css @@ -23,23 +23,27 @@ } .java-externalLibraries-icon { - background-image: url(icons/externalLibraries.svg); + background-image: url(icons/externalLibraries.svg) !important; } .java-file-icon { - background-image: url(icons/file.svg); + background-image: url(icons/file.svg) !important; } .java-folder-icon { - background-image: url(icons/folder.svg); + background-image: url(icons/folder.svg) !important; +} + +.java-source-folder-icon { + background-image: url(icons/source.svg) !important; } .java-jar-icon { - background-image: url(icons/jar.svg); + background-image: url(icons/jar.svg) !important; } .java-package-icon { - background-image: url(icons/package.svg); + background-image: url(icons/package.svg) !important; } .java-enum-icon { @@ -56,4 +60,9 @@ .java-field-icon { background-image: url(icons/field.svg) !important; -} \ No newline at end of file +} + +.java-remove-node-icon { + background-image: url(icons/remove-node.svg) !important; + display: none; +} diff --git a/che-theia-java-extension/src/browser/styles/icons/remove-node.svg b/che-theia-java-extension/src/browser/styles/icons/remove-node.svg new file mode 100644 index 0000000..f0a9059 --- /dev/null +++ b/che-theia-java-extension/src/browser/styles/icons/remove-node.svg @@ -0,0 +1,22 @@ + + + + + + + + + diff --git a/che-theia-java-extension/src/browser/styles/icons/source.svg b/che-theia-java-extension/src/browser/styles/icons/source.svg new file mode 100644 index 0000000..a498cf5 --- /dev/null +++ b/che-theia-java-extension/src/browser/styles/icons/source.svg @@ -0,0 +1,21 @@ + + + + + + + \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index fd2441c..c7118db 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1060,7 +1060,7 @@ aws4@^1.2.1, aws4@^1.6.0: version "1.7.0" resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.7.0.tgz#d4d0e9b9dbfca77bf08eeb0a8a471550fe39e289" -babel-code-frame@^6.11.0, babel-code-frame@^6.26.0: +babel-code-frame@^6.11.0, babel-code-frame@^6.22.0, babel-code-frame@^6.26.0: version "6.26.0" resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b" dependencies: @@ -1884,7 +1884,7 @@ buffer@^4.3.0: ieee754 "^1.1.4" isarray "^1.0.0" -builtin-modules@^1.0.0: +builtin-modules@^1.0.0, builtin-modules@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f" @@ -2314,6 +2314,10 @@ commander@^2.11.0: version "2.16.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.16.0.tgz#f16390593996ceb4f3eeb020b31d78528f7f8a50" +commander@^2.12.1: + version "2.19.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.19.0.tgz#f6198aa84e5b83c46054b94ddedbfed5ee9ff12a" + commander@~2.13.0: version "2.13.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.13.0.tgz#6964bca67685df7c1f1430c584f07d7597885b9c" @@ -2910,7 +2914,7 @@ detect-libc@^1.0.2, detect-libc@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" -diff@^3.3.1, diff@^3.5.0: +diff@^3.2.0, diff@^3.3.1, diff@^3.5.0: version "3.5.0" resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" @@ -3126,14 +3130,14 @@ esprima@^2.6.0: version "2.7.3" resolved "https://registry.yarnpkg.com/esprima/-/esprima-2.7.3.tgz#96e3b70d5779f6ad49cd032673d1c312767ba581" +esprima@^4.0.0, esprima@~4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" + esprima@~3.1.0: version "3.1.3" resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633" -esprima@~4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" - esrecurse@^4.1.0: version "4.2.1" resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.2.1.tgz#007a3b9fdbc2b3bb87e4879ea19c92fdbd3942cf" @@ -3799,6 +3803,17 @@ glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.1.2: once "^1.3.0" path-is-absolute "^1.0.0" +glob@^7.1.1: + version "7.1.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.3.tgz#3960832d3f1574108342dafd3a67b332c0969df1" + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + global-modules@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-1.0.0.tgz#6d770f0eb523ac78164d72b5e71a8877265cc3ea" @@ -4559,6 +4574,13 @@ js-tokens@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" +js-yaml@^3.7.0: + version "3.12.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.12.0.tgz#eaed656ec8344f10f527c6bfa1b6e2244de167d1" + dependencies: + argparse "^1.0.7" + esprima "^4.0.0" + js-yaml@~3.7.0: version "3.7.0" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.7.0.tgz#5c967ddd837a9bfdca5f2de84253abe8a1c03b80" @@ -5924,6 +5946,10 @@ path-parse@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.5.tgz#3c1adf871ea9cd6c9431b6ea2bd74a0ff055c4c1" +path-parse@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" + path-to-regexp@0.1.7: version "0.1.7" resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" @@ -6864,6 +6890,12 @@ resolve@^1.1.6: dependencies: path-parse "^1.0.5" +resolve@^1.3.2: + version "1.9.0" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.9.0.tgz#a14c6fdfa8f92a7df1d996cb7105fa744658ea06" + dependencies: + path-parse "^1.0.6" + responselike@1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/responselike/-/responselike-1.0.2.tgz#918720ef3b631c5642be068f15ade5a46f4ba1e7" @@ -7726,10 +7758,33 @@ ts-md5@^1.2.2: version "1.2.4" resolved "https://registry.yarnpkg.com/ts-md5/-/ts-md5-1.2.4.tgz#7030d7ba9134449deedf6f609d4b4509b94a5712" -tslib@^1.9.0: +tslib@^1.8.0, tslib@^1.8.1, tslib@^1.9.0: version "1.9.3" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.3.tgz#d7e4dd79245d85428c4d7e4822a79917954ca286" +tslint@5.10.0: + version "5.10.0" + resolved "https://registry.yarnpkg.com/tslint/-/tslint-5.10.0.tgz#11e26bccb88afa02dd0d9956cae3d4540b5f54c3" + dependencies: + babel-code-frame "^6.22.0" + builtin-modules "^1.1.1" + chalk "^2.3.0" + commander "^2.12.1" + diff "^3.2.0" + glob "^7.1.1" + js-yaml "^3.7.0" + minimatch "^3.0.4" + resolve "^1.3.2" + semver "^5.3.0" + tslib "^1.8.0" + tsutils "^2.12.1" + +tsutils@^2.12.1: + version "2.29.0" + resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-2.29.0.tgz#32b488501467acbedd4b85498673a0812aca0b99" + dependencies: + tslib "^1.8.1" + tty-browserify@0.0.0: version "0.0.0" resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6" @@ -8484,4 +8539,4 @@ zip-dir@^1.0.2: resolved "https://registry.yarnpkg.com/zip-dir/-/zip-dir-1.0.2.tgz#253f907aead62a21acd8721d8b88032b2411c051" dependencies: async "^1.5.2" - jszip "^2.4.0" \ No newline at end of file + jszip "^2.4.0"