From be2eb701eb2ae60cf5324bf1a8a0dc5af5ba57f6 Mon Sep 17 00:00:00 2001 From: Sheng Chen Date: Mon, 25 Mar 2024 14:15:12 +0800 Subject: [PATCH] Support updating classpaths for Maven/Gradle projects (#1283) Signed-off-by: Sheng Chen --- ThirdPartyNotices.txt | 38 +- package-lock.json | 30 +- package.json | 2 - .../ClasspathConfigurationView.tsx | 45 +- .../classpathConfigurationViewSlice.ts | 69 ++- .../components/Exception.tsx | 2 +- .../components/Footer.tsx | 94 ++++ .../components/Header.tsx | 14 - .../components/JdkRuntime.tsx | 44 +- .../components/Libraries.tsx | 124 ++++++ .../components/Output.tsx | 38 +- .../components/ProjectSelector.tsx | 42 +- .../components/ReferencedLibraries.tsx | 88 ---- .../components/Sources.tsx | 231 +++++++--- .../components/UnmanagedFolderSources.tsx | 101 +++++ .../components/common/SectionHeader.tsx | 20 - src/classpath/assets/style.scss | 69 ++- src/classpath/assets/utils.scss | 40 ++ src/classpath/assets/utils.ts | 58 ++- src/classpath/classpathConfigurationView.ts | 405 +++++++++++------- src/classpath/types.ts | 24 +- 21 files changed, 1080 insertions(+), 498 deletions(-) create mode 100644 src/classpath/assets/features/classpathConfiguration/components/Footer.tsx delete mode 100644 src/classpath/assets/features/classpathConfiguration/components/Header.tsx create mode 100644 src/classpath/assets/features/classpathConfiguration/components/Libraries.tsx delete mode 100644 src/classpath/assets/features/classpathConfiguration/components/ReferencedLibraries.tsx create mode 100644 src/classpath/assets/features/classpathConfiguration/components/UnmanagedFolderSources.tsx delete mode 100644 src/classpath/assets/features/classpathConfiguration/components/common/SectionHeader.tsx create mode 100644 src/classpath/assets/utils.scss diff --git a/ThirdPartyNotices.txt b/ThirdPartyNotices.txt index 2ef8e745..5bb578bb 100644 --- a/ThirdPartyNotices.txt +++ b/ThirdPartyNotices.txt @@ -16,15 +16,14 @@ are grateful to these developers for their contribution to open source. 8. highlight.js (https://github.com/highlightjs/highlight.js) 9. jquery (https://github.com/jquery/jquery) 10. lodash (lodash/lodash) -11. minimatch (https://github.com/isaacs/minimatch) -12. react (https://github.com/facebook/react) -13. react-bootstrap (https://github.com/react-bootstrap/react-bootstrap) -14. react-dom (https://github.com/facebook/react) -15. react-redux (https://github.com/reduxjs/react-redux) -16. semver (https://github.com/npm/node-semver) -17. vscode-extension-telemetry-wrapper (https://github.com/Eskibear/vscode-extension-telemetry-wrapper) -18. xmldom (https://github.com/xmldom/xmldom) -19. HdrHistogramJS (https://github.com/HdrHistogram/HdrHistogramJS) +11. react (https://github.com/facebook/react) +12. react-bootstrap (https://github.com/react-bootstrap/react-bootstrap) +13. react-dom (https://github.com/facebook/react) +14. react-redux (https://github.com/reduxjs/react-redux) +15. semver (https://github.com/npm/node-semver) +16. vscode-extension-telemetry-wrapper (https://github.com/Eskibear/vscode-extension-telemetry-wrapper) +17. xmldom (https://github.com/xmldom/xmldom) +18. HdrHistogramJS (https://github.com/HdrHistogram/HdrHistogramJS) @iconify/react NOTICES BEGIN HERE ============================= @@ -321,27 +320,6 @@ terms above. ========================================= END OF lodash NOTICES AND INFORMATION -minimatch NOTICES BEGIN HERE -============================= -The ISC License - -Copyright (c) Isaac Z. Schlueter and Contributors - -Permission to use, copy, modify, and/or distribute this software for any -purpose with or without fee is hereby granted, provided that the above -copyright notice and this permission notice appear in all copies. - -THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR -ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES -WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN -ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR -IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - -========================================= -END OF minimatch NOTICES AND INFORMATION - react NOTICES BEGIN HERE ============================= MIT License diff --git a/package-lock.json b/package-lock.json index 5bc6f857..0e31b6ec 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,7 +26,6 @@ "jdk-utils": "^0.4.4", "jquery": "^3.6.1", "lodash": "^4.17.21", - "minimatch": "^3.1.2", "react": "^16.14.0", "react-bootstrap": "^1.6.6", "react-dom": "^16.14.0", @@ -40,7 +39,6 @@ "@types/expand-tilde": "^2.0.0", "@types/fs-extra": "^9.0.13", "@types/lodash": "^4.14.186", - "@types/minimatch": "^3.0.5", "@types/node": "18.x", "@types/path-exists": "^3.0.0", "@types/react": "^17.0.50", @@ -399,12 +397,6 @@ "integrity": "sha512-eHcVlLXP0c2FlMPm56ITode2AgLMSa6aJ05JTTbYbI+7EMkCEE5qk2E41d5g2lCVTqRe0GnnRFurmlCsDODrPw==", "dev": true }, - "node_modules/@types/minimatch": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz", - "integrity": "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==", - "dev": true - }, "node_modules/@types/node": { "version": "18.19.21", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.21.tgz", @@ -959,7 +951,8 @@ "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true }, "node_modules/base64-js": { "version": "1.5.1", @@ -1021,6 +1014,7 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -1234,7 +1228,8 @@ "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true }, "node_modules/cosmiconfig": { "version": "7.0.1", @@ -2171,6 +2166,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -3893,12 +3889,6 @@ "integrity": "sha512-eHcVlLXP0c2FlMPm56ITode2AgLMSa6aJ05JTTbYbI+7EMkCEE5qk2E41d5g2lCVTqRe0GnnRFurmlCsDODrPw==", "dev": true }, - "@types/minimatch": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz", - "integrity": "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==", - "dev": true - }, "@types/node": { "version": "18.19.21", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.21.tgz", @@ -4353,7 +4343,8 @@ "balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true }, "base64-js": { "version": "1.5.1", @@ -4382,6 +4373,7 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -4530,7 +4522,8 @@ "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true }, "cosmiconfig": { "version": "7.0.1", @@ -5216,6 +5209,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, "requires": { "brace-expansion": "^1.1.7" } diff --git a/package.json b/package.json index 9b80af08..0184d682 100644 --- a/package.json +++ b/package.json @@ -321,7 +321,6 @@ "@types/expand-tilde": "^2.0.0", "@types/fs-extra": "^9.0.13", "@types/lodash": "^4.14.186", - "@types/minimatch": "^3.0.5", "@types/node": "18.x", "@types/path-exists": "^3.0.0", "@types/react": "^17.0.50", @@ -373,7 +372,6 @@ "jdk-utils": "^0.4.4", "jquery": "^3.6.1", "lodash": "^4.17.21", - "minimatch": "^3.1.2", "react": "^16.14.0", "react-bootstrap": "^1.6.6", "react-dom": "^16.14.0", diff --git a/src/classpath/assets/features/classpathConfiguration/ClasspathConfigurationView.tsx b/src/classpath/assets/features/classpathConfiguration/ClasspathConfigurationView.tsx index 9ad13dff..799d3927 100644 --- a/src/classpath/assets/features/classpathConfiguration/ClasspathConfigurationView.tsx +++ b/src/classpath/assets/features/classpathConfiguration/ClasspathConfigurationView.tsx @@ -7,17 +7,20 @@ import { Dispatch } from "@reduxjs/toolkit"; import Output from "./components/Output"; import ProjectSelector from "./components/ProjectSelector"; import Sources from "./components/Sources"; -import ReferencedLibraries from "./components/ReferencedLibraries"; -import Header from "./components/Header"; +import Libraries from "./components/Libraries"; import Exception from "./components/Exception"; -import { ClasspathViewException, ProjectInfo, VmInstall } from "../../../types"; +import { ClasspathViewException, ProjectInfo, ClasspathEntry, VmInstall } from "../../../types"; import { catchException, listProjects, listVmInstalls, loadClasspath } from "./classpathConfigurationViewSlice"; import JdkRuntime from "./components/JdkRuntime"; import { onWillListProjects, onWillListVmInstalls } from "../../utils"; -import { VSCodeProgressRing } from "@vscode/webview-ui-toolkit/react"; +import { VSCodePanelTab, VSCodePanelView, VSCodePanels, VSCodeProgressRing } from "@vscode/webview-ui-toolkit/react"; +import { ProjectType } from "../../../../utils/webview"; +import UnmanagedFolderSources from "./components/UnmanagedFolderSources"; +import Footer from "./components/Footer"; const ClasspathConfigurationView = (): JSX.Element => { const projects: ProjectInfo[] = useSelector((state: any) => state.classpathConfig.projects); + const projectType: ProjectType = useSelector((state: any) => state.classpathConfig.projectType[state.classpathConfig.activeProjectIndex]); const exception: ClasspathViewException | undefined = useSelector((state: any) => state.classpathConfig.exception); let content: JSX.Element; @@ -28,11 +31,26 @@ const ClasspathConfigurationView = (): JSX.Element => { } else { content = (
- - - - - +
+ + + Sources + JDK Runtime + Libraries + + {[ProjectType.Gradle, ProjectType.Maven].includes(projectType) && ()} + {projectType !== ProjectType.Gradle && projectType !== ProjectType.Maven && ()} + {projectType === ProjectType.UnmanagedFolder && ()} + + + + + + + + +
+
); } @@ -56,12 +74,13 @@ const ClasspathConfigurationView = (): JSX.Element => { window.addEventListener("message", onInitialize); onWillListProjects(); onWillListVmInstalls(); - return () => window.removeEventListener("message", onInitialize); + return () => { + window.removeEventListener("message", onInitialize); + } }, []); return (
-
{content}
); @@ -76,9 +95,9 @@ interface OnInitializeEvent { projectType: string; }[]; vmInstalls?: VmInstall[]; - sources?: string[]; + sources?: ClasspathEntry[]; output?: string; - referencedLibraries?: string[]; + libraries?: string[]; exception?: ClasspathViewException; }; } diff --git a/src/classpath/assets/features/classpathConfiguration/classpathConfigurationViewSlice.ts b/src/classpath/assets/features/classpathConfiguration/classpathConfigurationViewSlice.ts index 4c911b04..41b64649 100644 --- a/src/classpath/assets/features/classpathConfiguration/classpathConfigurationViewSlice.ts +++ b/src/classpath/assets/features/classpathConfiguration/classpathConfigurationViewSlice.ts @@ -1,26 +1,37 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. -import { createSlice } from "@reduxjs/toolkit"; +import { createSlice, current } from "@reduxjs/toolkit"; import _ from "lodash"; +import { ClasspathEntry, ProjectState } from "../../../types"; +import { ProjectType } from "../../../../utils/webview"; export const classpathConfigurationViewSlice = createSlice({ name: "classpathConfig", initialState: { activeProjectIndex: 0, projects: [], - activeVmInstallPath: "", + activeVmInstallPath: [] as string[], vmInstalls: [], - projectType: undefined, - sources: [] as string[], - output: "", - referencedLibraries: [] as string[], + projectType: [] as ProjectType[], + sources: [] as ClasspathEntry[][], + output: [] as string[], + libraries: [] as ClasspathEntry[][], + projectState: [] as ProjectState[], + loadingState: false, exception: undefined, }, reducers: { listProjects: (state, action) => { state.projects = action.payload; state.activeProjectIndex = 0; + const projectNum = state.projects.length; + state.activeVmInstallPath = Array(projectNum).fill(""); + state.projectType = Array(projectNum).fill(""); + state.sources = Array(projectNum).fill([]); + state.output = Array(projectNum).fill(""); + state.libraries = Array(projectNum).fill([]); + state.projectState = Array(projectNum).fill(ProjectState.Unloaded); }, listVmInstalls: (state, action) => { state.vmInstalls = action.payload; @@ -29,25 +40,31 @@ export const classpathConfigurationViewSlice = createSlice({ state.activeProjectIndex = action.payload; }, loadClasspath: (state, action) => { - state.projectType = action.payload.projectType; - state.output = action.payload.output; - state.activeVmInstallPath = action.payload.activeVmInstallPath; + state.projectState[state.activeProjectIndex] = ProjectState.Loaded; + state.projectType[state.activeProjectIndex] = action.payload.projectType; + state.output[state.activeProjectIndex] = action.payload.output; + state.activeVmInstallPath[state.activeProjectIndex] = action.payload.activeVmInstallPath; // Only update the array when they have different elements. - if (isDifferentStringArray(state.sources, action.payload.sources)) { - state.sources = action.payload.sources; + const currentSources = _.sortBy(current(state.sources), ["path", "output"]); + const newSources = _.sortBy(action.payload.sources, ["path", "output"]); + if (!_.isEqual(currentSources, newSources)) { + state.sources[state.activeProjectIndex] = action.payload.sources; } - if (isDifferentStringArray(state.referencedLibraries, action.payload.referencedLibraries)) { - state.referencedLibraries = action.payload.referencedLibraries; + + const currentLibs = _.sortBy(current(state.libraries), ["path"]); + const newLibs = _.sortBy(action.payload.libraries, ["path"]); + if (!_.isEqual(currentLibs, newLibs)) { + state.libraries[state.activeProjectIndex] = action.payload.libraries; } }, updateSource: (state, action) => { - state.sources = action.payload; + state.sources[state.activeProjectIndex] = action.payload; }, setOutputPath: (state, action) => { - state.output = action.payload; + state.output[state.activeProjectIndex] = action.payload; }, setJdks: (state, action) => { - state.activeVmInstallPath = action.payload.activeVmInstallPath; + state.activeVmInstallPath[state.activeProjectIndex] = action.payload.activeVmInstallPath; if (action.payload.vmInstalls && isDifferentStringArray(state.vmInstalls, action.payload.vmInstalls)) { state.vmInstalls = action.payload.vmInstalls; @@ -55,17 +72,22 @@ export const classpathConfigurationViewSlice = createSlice({ }, removeReferencedLibrary: (state, action) => { const removedIndex: number = action.payload as number; - if (removedIndex > -1 && removedIndex < state.referencedLibraries.length) { - state.referencedLibraries.splice(removedIndex, 1); + if (removedIndex > -1 && removedIndex < state.libraries[state.activeProjectIndex].length) { + state.libraries[state.activeProjectIndex].splice(removedIndex, 1); } }, - addReferencedLibraries: (state, action) => { - state.referencedLibraries.push(...action.payload); - state.referencedLibraries = _.uniq(state.referencedLibraries); + addLibraries: (state, action) => { + let newLibs = state.libraries[state.activeProjectIndex]; + newLibs.unshift(...action.payload); + newLibs = _.uniq(newLibs); + state.libraries[state.activeProjectIndex] = _.uniq(newLibs); }, catchException: (state, action) => { state.exception = action.payload; - } + }, + updateLoadingState: (state, action) => { + state.loadingState = action.payload; + }, }, }); @@ -82,8 +104,9 @@ export const { setOutputPath, setJdks, removeReferencedLibrary, - addReferencedLibraries, + addLibraries, catchException, + updateLoadingState, } = classpathConfigurationViewSlice.actions; export default classpathConfigurationViewSlice.reducer; diff --git a/src/classpath/assets/features/classpathConfiguration/components/Exception.tsx b/src/classpath/assets/features/classpathConfiguration/components/Exception.tsx index ce36694e..699b0f25 100644 --- a/src/classpath/assets/features/classpathConfiguration/components/Exception.tsx +++ b/src/classpath/assets/features/classpathConfiguration/components/Exception.tsx @@ -19,7 +19,7 @@ const Exception = (): JSX.Element | null => { } content = (
- There is no Java projects opened in the current workspace. Please open a Java project. + There is no Java projects opened in the current workspace. Please try to reload the page or open a Java project.
); break; diff --git a/src/classpath/assets/features/classpathConfiguration/components/Footer.tsx b/src/classpath/assets/features/classpathConfiguration/components/Footer.tsx new file mode 100644 index 00000000..3bf50ada --- /dev/null +++ b/src/classpath/assets/features/classpathConfiguration/components/Footer.tsx @@ -0,0 +1,94 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +import { VSCodeButton, VSCodeDivider, VSCodeLink} from "@vscode/webview-ui-toolkit/react"; +import { Dispatch } from "@reduxjs/toolkit"; +import React, { useEffect } from "react"; +import { ClasspathEntry, ProjectInfo } from "../../../../types"; +import { useDispatch, useSelector } from "react-redux"; +import { ProjectType } from "../../../../../utils/webview"; +import { onClickGotoProjectConfiguration, onWillUpdateClassPaths, updateMaxHeight } from "../../../utils"; +import { updateLoadingState } from "../classpathConfigurationViewSlice"; + +const Footer = (): JSX.Element => { + + const activeProjectIndex: number = useSelector((state: any) => state.classpathConfig.activeProjectIndex); + const projects: ProjectInfo[] = useSelector((state: any) => state.classpathConfig.projects); + const sources: ClasspathEntry[][] = useSelector((state: any) => state.classpathConfig.sources); + const defaultOutput: string[] = useSelector((state: any) => state.classpathConfig.output); + const activeVmInstallPath: string[] = useSelector((state: any) => state.classpathConfig.activeVmInstallPath); + const projectType: ProjectType[] = useSelector((state: any) => state.classpathConfig.projectType); + const libraries: ClasspathEntry[][] = useSelector((state: any) => state.classpathConfig.libraries); + const loadingState: boolean = useSelector((state: any) => state.classpathConfig.loadingState); + + const dispatch: Dispatch = useDispatch(); + + let buildFile: string = ""; + if (projectType[activeProjectIndex] === ProjectType.Maven) { + buildFile = "pom.xml"; + } else if (projectType[activeProjectIndex] === ProjectType.Gradle) { + buildFile = "build.gradle"; + } + + const handleOpenBuildFile = () => { + onClickGotoProjectConfiguration(projects[activeProjectIndex].rootPath, projectType[activeProjectIndex]); + }; + + const handleApply = () => { + onWillUpdateClassPaths( + projects.map(p => p.rootPath), + projectType, + sources, + defaultOutput, + activeVmInstallPath, + libraries + ); + }; + + const onDidChangeLoadingState = (event: OnDidChangeLoadingStateEvent) => { + const {data} = event; + if (data.command === "onDidChangeLoadingState") { + dispatch(updateLoadingState(data.loading)); + } + } + + useEffect(() => { + window.addEventListener("message", onDidChangeLoadingState); + return () => { + window.removeEventListener("message", onDidChangeLoadingState); + } + }, []); + + useEffect(() => { + updateMaxHeight(); + window.addEventListener('resize', updateMaxHeight); + return () => { + window.removeEventListener("resize", updateMaxHeight); + } + }, [projectType]); + + return ( + + ); +}; + +interface OnDidChangeLoadingStateEvent { + data: { + command: string; + loading: boolean; + }; +} + +export default Footer; diff --git a/src/classpath/assets/features/classpathConfiguration/components/Header.tsx b/src/classpath/assets/features/classpathConfiguration/components/Header.tsx deleted file mode 100644 index cb07cda8..00000000 --- a/src/classpath/assets/features/classpathConfiguration/components/Header.tsx +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. - -import React from "react"; - -const Header = (): JSX.Element => { - return ( -
-

Configure Classpath

-
- ); -}; - -export default Header; diff --git a/src/classpath/assets/features/classpathConfiguration/components/JdkRuntime.tsx b/src/classpath/assets/features/classpathConfiguration/components/JdkRuntime.tsx index c019b5fa..f9dbc049 100644 --- a/src/classpath/assets/features/classpathConfiguration/components/JdkRuntime.tsx +++ b/src/classpath/assets/features/classpathConfiguration/components/JdkRuntime.tsx @@ -2,27 +2,27 @@ // Licensed under the MIT license. import React, { Dispatch, useEffect, useState } from "react"; -import { encodeCommandUriWithTelemetry } from "../../../../../utils/webview"; -import { WEBVIEW_ID, onWillChangeJdk } from "../../../utils"; -import { VSCodeDivider, VSCodeDropdown, VSCodeLink, VSCodeOption, } from "@vscode/webview-ui-toolkit/react"; +import { onWillAddNewJdk, onWillExecuteCommand } from "../../../utils"; +import { VSCodeDivider, VSCodeDropdown, VSCodeOption, } from "@vscode/webview-ui-toolkit/react"; import { useDispatch, useSelector } from "react-redux"; import { VmInstall } from "../../../../types"; import { setJdks } from "../classpathConfigurationViewSlice"; -import SectionHeader from "./common/SectionHeader"; const JdkRuntime = (): JSX.Element => { const vmInstalls: VmInstall[] = useSelector((state: any) => state.classpathConfig.vmInstalls); - const activeVmInstallPath: string = useSelector((state: any) => state.classpathConfig.activeVmInstallPath); + const activeVmInstallPath: string = useSelector((state: any) => state.classpathConfig.activeVmInstallPath[state.classpathConfig.activeProjectIndex]); const [optionDescription, setOptionDescription] = useState(null); const dispatch: Dispatch = useDispatch(); const handleSelectJdk = (path: string) => { - onWillChangeJdk(path); - if (path !== "add-new-jdk") { - // if the user selects a existing JDK, we directly update the activeVmInstallPath. + if (path === "add-new-jdk") { + onWillAddNewJdk(); + } else if (path === "download-jdk") { + onWillExecuteCommand("java.installJdk") + } else { dispatch(setJdks({activeVmInstallPath: path})); } } @@ -56,16 +56,32 @@ const JdkRuntime = (): JSX.Element => { key="add-new-jdk" value="add-new-jdk" id="add-new-jdk" - onMouseEnter={() => setOptionDescription("Select a JDK from the file system.")} - onMouseLeave={() => setOptionDescription(activeVmInstallPath + 'asds')} + onMouseEnter={() => setOptionDescription("Select a JDK from the local file system.")} + onMouseLeave={() => setOptionDescription(activeVmInstallPath)} onClick={() => handleSelectJdk("add-new-jdk")} >
- Add a new JDK... + Find a local JDK...
); + const downloadJdk = ( + setOptionDescription("Download a new JDK.")} + onMouseLeave={() => setOptionDescription(activeVmInstallPath)} + onClick={() => handleSelectJdk("download-jdk")} + > +
+ Download a new JDK... +
+
+ ); + useEffect(() => { window.addEventListener("message", onDidChangeJdk); // the dropdown list has a fixed height by default, which makes the list jitter @@ -79,9 +95,8 @@ const JdkRuntime = (): JSX.Element => { return (
- - Specify the JDK runtime of the project. Or install a new JDK. -
+
+ JDK:

{optionDescription ?? activeVmInstallPath}

@@ -90,6 +105,7 @@ const JdkRuntime = (): JSX.Element => { {jdkSelections} {addNewJdk} + {downloadJdk}

{optionDescription ?? activeVmInstallPath}

diff --git a/src/classpath/assets/features/classpathConfiguration/components/Libraries.tsx b/src/classpath/assets/features/classpathConfiguration/components/Libraries.tsx new file mode 100644 index 00000000..c3e77241 --- /dev/null +++ b/src/classpath/assets/features/classpathConfiguration/components/Libraries.tsx @@ -0,0 +1,124 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +import { Dispatch } from "@reduxjs/toolkit"; +import React, { useEffect, useState } from "react"; +import { useSelector, useDispatch } from "react-redux"; +import { removeReferencedLibrary, addLibraries } from "../classpathConfigurationViewSlice"; +import { onWillSelectLibraries } from "../../../utils"; +import { VSCodeButton, VSCodeDataGrid, VSCodeDataGridCell, VSCodeDataGridRow, VSCodeDivider } from "@vscode/webview-ui-toolkit/react"; +import { ClasspathEntry, ClasspathEntryKind } from "../../../../types"; + +const Libraries = (): JSX.Element => { + + const [hoveredRow, setHoveredRow] = useState(null); + + const libraries: ClasspathEntry[] = useSelector((state: any) => state.classpathConfig.libraries[state.classpathConfig.activeProjectIndex]); + const dispatch: Dispatch = useDispatch(); + + const handleRemove = (index: number) => { + dispatch(removeReferencedLibrary(index)); + }; + + const handleAdd = () => { + onWillSelectLibraries(); + }; + + const onDidAddLibraries = (event: OnDidAddLibrariesEvent) => { + const {data} = event; + if (data.command === "onDidAddLibraries") { + dispatch(addLibraries(data.jars)); + } + }; + + const resolveLibPath = (entry: ClasspathEntry): string => { + if (entry.kind === ClasspathEntryKind.Project) { + return resolveProjectPath(entry); + } else if (entry.attributes?.hasOwnProperty("maven.pomderived")) { + return resolveMavenLibPath(entry); + } else if (entry.attributes?.hasOwnProperty("gradle_used_by_scope")|| entry.attributes?.hasOwnProperty("gradle.buildServer")) { + return resolveGradleLibPath(entry); + } + return entry.path; + } + + const resolveProjectPath = (entry: ClasspathEntry): string => { + return `Project: ${getLastPathComponent(entry.path)}`; + } + + const resolveMavenLibPath = (entry: ClasspathEntry): string => { + if (entry.attributes?.["maven.groupId"] && entry.attributes?.["maven.artifactId"] && entry.attributes?.["maven.version"]) { + return `Maven: ${entry.attributes["maven.groupId"]}:${entry.attributes["maven.artifactId"]}:${entry.attributes["maven.version"]}`; + } + return entry.path; + } + + const resolveGradleLibPath = (entry: ClasspathEntry): string => { + return `Gradle: ${getLastPathComponent(entry.path)}`; + } + + const getLastPathComponent = (path: string): string => { + const pathComponents = path.split(/[\\/]/); + return pathComponents[pathComponents.length - 1]; + } + + useEffect(() => { + window.addEventListener("message", onDidAddLibraries); + return () => { + window.removeEventListener("message", onDidAddLibraries); + } + }, []); + + let librariesSections: JSX.Element | JSX.Element[]; + if (libraries.length === 0) { + librariesSections = ( + + No libraries are configured. + + ); + } else { + librariesSections = libraries.map((library, index) => ( + setHoveredRow(`library-${index}`)} onMouseLeave={() => setHoveredRow(null)} key={library.path}> + +
+ + {resolveLibPath(library)} +
+ {hoveredRow === `library-${index}` && ( + handleRemove(index)}> + + + )} +
+
+ )); + } + + return ( +
+
+
+ handleAdd()}> + + Add Library... + +
+ +
+ + {librariesSections} + +
+
+
+ ); +}; + +interface OnDidAddLibrariesEvent { + data: { + command: string; + jars: ClasspathEntry[]; + }; +} + +export default Libraries; diff --git a/src/classpath/assets/features/classpathConfiguration/components/Output.tsx b/src/classpath/assets/features/classpathConfiguration/components/Output.tsx index 22c40ce6..3bd662b7 100644 --- a/src/classpath/assets/features/classpathConfiguration/components/Output.tsx +++ b/src/classpath/assets/features/classpathConfiguration/components/Output.tsx @@ -7,12 +7,11 @@ import { useSelector, useDispatch } from "react-redux"; import { ProjectType } from "../../../../../utils/webview"; import { onWillSelectOutputPath } from "../../../utils"; import { setOutputPath } from "../classpathConfigurationViewSlice"; -import { VSCodeButton, VSCodeTextArea } from "@vscode/webview-ui-toolkit/react"; -import SectionHeader from "./common/SectionHeader"; +import { VSCodeButton, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"; -const Output = (): JSX.Element => { - const output: string = useSelector((state: any) => state.classpathConfig.output); - const projectType: ProjectType = useSelector((state: any) => state.classpathConfig.projectType); +const Output = (): JSX.Element | null => { + const output: string = useSelector((state: any) => state.classpathConfig.output[state.classpathConfig.activeProjectIndex]); + const projectType: ProjectType = useSelector((state: any) => state.classpathConfig.projectType[state.classpathConfig.activeProjectIndex]); const dispatch: Dispatch = useDispatch(); const handleClick = () => { onWillSelectOutputPath(); @@ -30,22 +29,21 @@ const Output = (): JSX.Element => { return () => window.removeEventListener("message", onDidSelectOutputPath); }, []); + if (projectType !== ProjectType.UnmanagedFolder) { + return null; + } + return ( -
- - Specify compile output path location. -
- -
- {projectType === ProjectType.UnmanagedFolder && - handleClick()}>Browse - } +
+

Output Path

+ + handleClick()}> + + +
); }; diff --git a/src/classpath/assets/features/classpathConfiguration/components/ProjectSelector.tsx b/src/classpath/assets/features/classpathConfiguration/components/ProjectSelector.tsx index d7b5d80a..ff841855 100644 --- a/src/classpath/assets/features/classpathConfiguration/components/ProjectSelector.tsx +++ b/src/classpath/assets/features/classpathConfiguration/components/ProjectSelector.tsx @@ -3,23 +3,16 @@ import React, { useEffect } from "react"; import { useSelector, useDispatch } from "react-redux"; -import { ProjectInfo } from "../../../../types"; +import { ProjectInfo, ProjectState } from "../../../../types"; import { Dispatch } from "@reduxjs/toolkit"; import { activeProjectChange } from "../classpathConfigurationViewSlice"; -import { onClickGotoProjectConfiguration, onWillLoadProjectClasspath } from "../../../utils"; -import { ProjectType } from "../../../../../utils/webview"; -import { VSCodeDropdown, VSCodeLink, VSCodeOption } from "@vscode/webview-ui-toolkit/react"; +import { onWillLoadProjectClasspath } from "../../../utils"; +import { VSCodeDropdown, VSCodeOption } from "@vscode/webview-ui-toolkit/react"; const ProjectSelector = (): JSX.Element | null => { const activeProjectIndex: number = useSelector((state: any) => state.classpathConfig.activeProjectIndex); const projects: ProjectInfo[] = useSelector((state: any) => state.classpathConfig.projects); - const projectType: ProjectType = useSelector((state: any) => state.classpathConfig.projectType); - let buildFile: string = ""; - if (projectType === ProjectType.Maven) { - buildFile = "pom.xml"; - } else if (projectType === ProjectType.Gradle) { - buildFile = "build.gradle"; - } + const projectState: ProjectState[] = useSelector((state: any) => state.classpathConfig.projectState); const dispatch: Dispatch = useDispatch(); @@ -27,12 +20,14 @@ const ProjectSelector = (): JSX.Element | null => { dispatch(activeProjectChange(index)); }; - const handleOpenBuildFile = () => { - onClickGotoProjectConfiguration(projects[activeProjectIndex].rootPath, projectType); - }; + const loadProjectClasspath = (rootPath: string) => { + if (projectState[activeProjectIndex] === ProjectState.Unloaded) { + onWillLoadProjectClasspath(rootPath); + } + } useEffect(() => { - onWillLoadProjectClasspath(projects[activeProjectIndex].rootPath); + loadProjectClasspath(projects[activeProjectIndex].rootPath); }, [activeProjectIndex, projects]); const projectSelections = projects.map((project, index) => { @@ -44,22 +39,13 @@ const ProjectSelector = (): JSX.Element | null => { }); return ( -
- Select the project. -
+
+
+ Project: - {projectSelections} + {projectSelections}
- - {(projectType === ProjectType.Gradle || projectType === ProjectType.Maven) && -
- - '{projects[activeProjectIndex].name}' is imported by {projectType}, changes made to the classpath might be lost after reloading. - To make permanent changes, please edit the handleOpenBuildFile()}>{buildFile} file. - -
- }
); }; diff --git a/src/classpath/assets/features/classpathConfiguration/components/ReferencedLibraries.tsx b/src/classpath/assets/features/classpathConfiguration/components/ReferencedLibraries.tsx deleted file mode 100644 index e20da13c..00000000 --- a/src/classpath/assets/features/classpathConfiguration/components/ReferencedLibraries.tsx +++ /dev/null @@ -1,88 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. - -import { Dispatch } from "@reduxjs/toolkit"; -import React, { useEffect, useState } from "react"; -import { useSelector, useDispatch } from "react-redux"; -import { removeReferencedLibrary, addReferencedLibraries } from "../classpathConfigurationViewSlice"; -import { onWillAddReferencedLibraries, onWillRemoveReferencedLibraries } from "../../../utils"; -import { ProjectType } from "../../../../../utils/webview"; -import { VSCodeButton, VSCodeDataGrid, VSCodeDataGridRow } from "@vscode/webview-ui-toolkit/react"; -import SectionHeader from "./common/SectionHeader"; - -const ReferencedLibraries = (): JSX.Element => { - - const [hoveredRow, setHoveredRow] = useState(null); - - const referencedLibraries: string[] = useSelector((state: any) => state.classpathConfig.referencedLibraries); - const projectType: ProjectType = useSelector((state: any) => state.classpathConfig.projectType); - const dispatch: Dispatch = useDispatch(); - - const handleRemove = (index: number) => { - onWillRemoveReferencedLibraries(referencedLibraries[index]); - dispatch(removeReferencedLibrary(index)); - }; - - const handleAdd = () => { - onWillAddReferencedLibraries(); - }; - - const onDidAddReferencedLibraries = (event: OnDidAddReferencedLibrariesEvent) => { - const {data} = event; - if (data.command === "onDidAddReferencedLibraries") { - dispatch(addReferencedLibraries(data.jars)); - } - }; - - useEffect(() => { - window.addEventListener("message", onDidAddReferencedLibraries); - return () => window.removeEventListener("message", onDidAddReferencedLibraries); - }, []); - - let referencedLibrariesSections: JSX.Element | JSX.Element[]; - if (referencedLibraries.length === 0) { - referencedLibrariesSections = ( - - No referenced libraries are configured. - - ); - } else { - referencedLibrariesSections = referencedLibraries.map((library, index) => ( - setHoveredRow(`library-${index}`)} onMouseLeave={() => setHoveredRow(null)} key={library}> - {library} - {hoveredRow === `library-${index}` && projectType === ProjectType.UnmanagedFolder && ( - handleRemove(index)}> - - - )} - - )); - } - - return ( -
- - Specify referenced libraries of the project. -
- - - Path - - {referencedLibrariesSections} - -
- {projectType === ProjectType.UnmanagedFolder && - handleAdd()}>Add - } -
- ); -}; - -interface OnDidAddReferencedLibrariesEvent { - data: { - command: string; - jars: string[]; - }; -} - -export default ReferencedLibraries; diff --git a/src/classpath/assets/features/classpathConfiguration/components/Sources.tsx b/src/classpath/assets/features/classpathConfiguration/components/Sources.tsx index 7bb1215c..6d276cde 100644 --- a/src/classpath/assets/features/classpathConfiguration/components/Sources.tsx +++ b/src/classpath/assets/features/classpathConfiguration/components/Sources.tsx @@ -5,91 +5,220 @@ import React, { useEffect, useState } from "react"; import { useSelector, useDispatch } from "react-redux"; import { Dispatch } from "@reduxjs/toolkit"; import { updateSource } from "../classpathConfigurationViewSlice"; -import { onWillAddSourcePath, onWillRemoveSourcePath } from "../../../utils"; -import { ProjectType } from "../../../../../utils/webview"; -import { VSCodeButton, VSCodeDataGrid, VSCodeDataGridRow } from "@vscode/webview-ui-toolkit/react"; -import SectionHeader from "./common/SectionHeader"; +import { onWillSelectFolder } from "../../../utils"; +import { VSCodeButton, VSCodeDataGrid, VSCodeDataGridCell, VSCodeDataGridRow, VSCodeDivider, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"; +import { ClasspathEntry, ClasspathEntryKind } from "../../../../types"; const Sources = (): JSX.Element => { + const sources: ClasspathEntry[] = useSelector((state: any) => state.classpathConfig.sources[state.classpathConfig.activeProjectIndex]); + const defaultOutput: string = useSelector((state: any) => state.classpathConfig.output[state.classpathConfig.activeProjectIndex]); + const [hoveredRow, setHoveredRow] = useState(null); + const [editRow, setEditRow] = useState(null); + const [editingSourcePath, setEditingSourcePath] = useState(null); + const [editingOutputPath, setEditingOutputPath] = useState(defaultOutput); - const sources: string[] = useSelector((state: any) => state.classpathConfig.sources); - const projectType: ProjectType = useSelector((state: any) => state.classpathConfig.projectType); const dispatch: Dispatch = useDispatch(); - const handleRemove = (source: string) => { - const updatedSources: string[] = []; - for (const path of sources) { - if (path === source) { + const handleRemove = (path: string) => { + const updatedSources: ClasspathEntry[] = []; + for (const sourceRoot of sources) { + if (sourceRoot.path === path) { continue; } - updatedSources.push(path); + updatedSources.push(sourceRoot); } - onWillRemoveSourcePath(updatedSources); dispatch(updateSource(updatedSources)); }; const handleAdd = () => { - onWillAddSourcePath(); + setEditingSourcePath(null); + setEditingOutputPath(defaultOutput); + setEditRow(sources.length); }; - const onDidUpdateSourceFolder = (event: OnDidAddSourceFolderEvent) => { + const handleEdit = (source: string, output: string, index: number) => { + setEditRow(index); + setEditingSourcePath(source); + setEditingOutputPath(output); + } + + const handleOK = () => { + const updatedSources: ClasspathEntry[] = sources.map((source, index) => { + if (index === editRow && editingSourcePath) { + return { + kind: ClasspathEntryKind.Source, + path: editingSourcePath, + output: editingOutputPath ?? undefined, + attributes: source.attributes, + }; + } + return source; + }); + + if (editRow === sources.length) { + updatedSources.push({ + kind: ClasspathEntryKind.Source, + path: editingSourcePath!, + output: editingOutputPath ?? undefined, + }); + } + dispatch(updateSource(updatedSources)); + setEditRow(null); + setEditingSourcePath(null); + setEditingOutputPath(defaultOutput); + } + + const handleCancel = () => { + setEditRow(null); + setEditingSourcePath(null); + setEditingOutputPath(defaultOutput); + } + + const handleBrowse = (type: string) => { + onWillSelectFolder(type); + } + + const messageHandler = (event: any) => { const {data} = event; - if (data.command === "onDidUpdateSourceFolder") { + if (data.command === "onDidSelectFolder") { + /** + * data: { + * command: string; + * path: string; + * type: string; + * } + */ + if (data.type === "source") { + setEditingSourcePath(data.path); + } else if (data.type === "output") { + setEditingOutputPath(data.path); + } + } else if (data.command === "onDidUpdateSourceFolder") { + /** + * data: { + * command: string; + * sourcePaths: string[]; + * } + */ dispatch(updateSource(data.sourcePaths)); } - }; + } useEffect(() => { - window.addEventListener("message", onDidUpdateSourceFolder); - return () => window.removeEventListener("message", onDidUpdateSourceFolder); + window.addEventListener("message", messageHandler); + return () => window.removeEventListener("message", messageHandler); }, []); - let sourceSections: JSX.Element | JSX.Element[]; - if (sources.length === 0) { - sourceSections = ( - - No source paths are configured. - - ); - } else { - sourceSections = sources.map((source, index) => ( - setHoveredRow(`sources-${index}`)} onMouseLeave={() => setHoveredRow(null)} key={source}> - {source} - {hoveredRow === `sources-${index}` && projectType === ProjectType.UnmanagedFolder && ( - handleRemove(source)}> - + const getSourceSections = () => { + if (sources.length === 0) { + return ( + + No source paths are configured. + + ); + } else { + return sources.map((source, index) => { + return getSourceRowComponents(source, index); + }); + } + }; + + const getEditRow = (id: string) => { + return ( + + + setEditingSourcePath(e.target.value)}> + handleBrowse("source")}> + + + + + + setEditingOutputPath(e.target.value)}> + handleBrowse("output")}> + + + + handleOK()}>OK + handleCancel()}> + Cancel - )} + - )); - } + ) + }; + + const getSourceRowComponents = (source: ClasspathEntry, index: number) => { + if (editRow === index) { + return getEditRow(`sources-${index}`); + } else { + return ( + setHoveredRow(`sources-${index}`)} onMouseLeave={() => setHoveredRow(null)} key={source.path}> + + {source.path} + + + {source.output || defaultOutput} +
+ handleEdit(source.path, source.output || defaultOutput, index)}> + + + handleRemove(source.path)}> + + +
+
+
+ ); + } + }; + + const getAdditionalEditRow = () => { + if (editRow === null) { + return null; + } + if (editRow < sources.length) { + return null; + } + return getEditRow("sources-additional-editing"); + }; return (
- - Specify the source locations. -
- +
+ handleAdd()}> + + Add Source Root... + +
+ +
+ - Path + + Path + + + Output + - {sourceSections} + {getSourceSections()} + {getAdditionalEditRow()}
- {projectType === ProjectType.UnmanagedFolder && - handleAdd()}>Add - } +
); }; -interface OnDidAddSourceFolderEvent { - data: { - command: string; - sourcePaths: string[]; - }; -} - export default Sources; diff --git a/src/classpath/assets/features/classpathConfiguration/components/UnmanagedFolderSources.tsx b/src/classpath/assets/features/classpathConfiguration/components/UnmanagedFolderSources.tsx new file mode 100644 index 00000000..3bdf04a7 --- /dev/null +++ b/src/classpath/assets/features/classpathConfiguration/components/UnmanagedFolderSources.tsx @@ -0,0 +1,101 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +import React, { useEffect, useState } from "react"; +import { useSelector, useDispatch } from "react-redux"; +import { Dispatch } from "@reduxjs/toolkit"; +import { updateSource } from "../classpathConfigurationViewSlice"; +import { onWillAddSourcePathForUnmanagedFolder } from "../../../utils"; +import { ProjectType } from "../../../../../utils/webview"; +import { VSCodeButton, VSCodeDataGrid, VSCodeDataGridCell, VSCodeDataGridRow, VSCodeDivider } from "@vscode/webview-ui-toolkit/react"; +import { ClasspathEntry } from "../../../../types"; + +const UnmanagedFolderSources = (): JSX.Element => { + + const [hoveredRow, setHoveredRow] = useState(null); + + const sources: ClasspathEntry[] = useSelector((state: any) => state.classpathConfig.sources[state.classpathConfig.activeProjectIndex]); + const projectType: ProjectType = useSelector((state: any) => state.classpathConfig.projectType[state.classpathConfig.activeProjectIndex]); + const dispatch: Dispatch = useDispatch(); + + const handleRemove = (path: string) => { + const updatedSources: ClasspathEntry[] = []; + for (const sourceRoot of sources) { + if (sourceRoot.path === path) { + continue; + } + updatedSources.push(sourceRoot); + } + dispatch(updateSource(updatedSources)); + }; + + const handleAdd = () => { + onWillAddSourcePathForUnmanagedFolder(); + }; + + const onDidUpdateSourceFolder = (event: OnDidAddSourceFolderEvent) => { + const {data} = event; + if (data.command === "onDidUpdateSourceFolder") { + dispatch(updateSource(data.sourcePaths.map(sp => { + return { + path: sp, + }; + }))); + } + }; + + useEffect(() => { + window.addEventListener("message", onDidUpdateSourceFolder); + return () => window.removeEventListener("message", onDidUpdateSourceFolder); + }, []); + + let sourceSections: JSX.Element | JSX.Element[]; + if (sources.length === 0) { + sourceSections = ( + + No source paths are configured. + + ); + } else { + sourceSections = sources.map((source, index) => ( + setHoveredRow(`sources-${index}`)} onMouseLeave={() => setHoveredRow(null)} key={source.path}> + +
+ + {source.path} +
+ {hoveredRow === `sources-${index}` && projectType === ProjectType.UnmanagedFolder && ( + handleRemove(source.path)}> + + + )} +
+
+ )); + } + + return ( +
+

Source Paths

+
+ handleAdd()}> + + Add Source Root... + +
+ + + {sourceSections} + +
+ ); +}; + +interface OnDidAddSourceFolderEvent { + data: { + command: string; + sourcePaths: string[]; + }; +} + +export default UnmanagedFolderSources; \ No newline at end of file diff --git a/src/classpath/assets/features/classpathConfiguration/components/common/SectionHeader.tsx b/src/classpath/assets/features/classpathConfiguration/components/common/SectionHeader.tsx deleted file mode 100644 index 16dfec69..00000000 --- a/src/classpath/assets/features/classpathConfiguration/components/common/SectionHeader.tsx +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. - -import React from "react"; - -const SectionHeader = ({title, subTitle}: SectionHeaderProps): JSX.Element => { - return ( -
-

{title}

- { subTitle && {subTitle} } -
- ); -}; - -export default SectionHeader; - -interface SectionHeaderProps { - title: string; - subTitle: string | undefined; -} diff --git a/src/classpath/assets/style.scss b/src/classpath/assets/style.scss index 51543db8..99696a7e 100644 --- a/src/classpath/assets/style.scss +++ b/src/classpath/assets/style.scss @@ -1,19 +1,29 @@ .root { - overflow: hidden; - min-width: 480px; + min-width: 560px; margin: 20px 40px 0; } .setting-header { color: var(--vscode-settings-headerForeground); - margin-bottom: 0; +} + +.setting-footer { + position: fixed; + bottom: 0; + width: 80%; + background: var(--background); } .setting-section { - padding: 10px 8px 14px; - - &:hover { - background-color: var(--vscode-settings-focusedRowBackground); + width: 100%; +} + +.setting-panels { + overflow-x: visible; + + .setting-panels-view { + max-width: 80vw; + flex-direction: column; } } @@ -21,11 +31,9 @@ color: var(--vscode-settings-headerForeground); display: flex; align-items: baseline; - padding-top: 2px; .setting-section-title { margin: 0; - padding-right:8px; } span { @@ -37,10 +45,6 @@ color: var(--vscode-descriptionForeground); } -.setting-section-target { - padding: 2px 0; -} - .setting-section-dropdown { width: 320px; @@ -84,15 +88,28 @@ } .setting-section-grid-row { - display: flex; - align-items: center; - justify-content: space-between; padding-left: 4px; - .setting-section-grid-row-header { font-weight: 700; } - + .setting-section-grid-cell { + padding: 0; + display: flex; + align-items: center; + } + .setting-section-grid-cell-left { + padding-right: calc(var(--design-unit) * 4px); + } + .setting-section-grid-cell-readonly { + justify-content: space-between; + } + .setting-section-grid-cell-editable { + justify-content: end; + } + .setting-section-grid-text { + flex-grow: 1; + } + span { font-size: 12.8px; line-height: 22px; @@ -101,12 +118,26 @@ } .setting-section-text { - width: 420px; + width: 100%; word-break: break-all; } +.setting-list-actions { + height: calc(var(--design-unit) * 8px); +} + +.setting-overflow-area { + overflow: auto; + max-height: 50vh; +} + .inactive { opacity: 0.67; } +.hidden { + visibility: hidden; +} + @import "~@vscode/codicons/dist/codicon.css"; +@import "utils.scss"; diff --git a/src/classpath/assets/utils.scss b/src/classpath/assets/utils.scss new file mode 100644 index 00000000..d3295bf3 --- /dev/null +++ b/src/classpath/assets/utils.scss @@ -0,0 +1,40 @@ +@for $i from 0 through 12 { + .pl-#{$i} { + padding-left: calc(var(--design-unit) * #{$i}px); + } + + .pr-#{$i} { + padding-right: calc(var(--design-unit) * #{$i}px); + } + + .pt-#{$i} { + padding-top: calc(var(--design-unit) * #{$i}px); + } + + .pb-#{$i} { + padding-bottom: calc(var(--design-unit) * #{$i}px); + } +} + +@for $i from 0 through 12 { + .ml-#{$i} { + margin-left: calc(var(--design-unit) * #{$i}px); + } + + .mr-#{$i} { + margin-right: calc(var(--design-unit) * #{$i}px); + } + + .mt-#{$i} { + margin-top: calc(var(--design-unit) * #{$i}px); + } + + .mb-#{$i} { + margin-bottom: calc(var(--design-unit) * #{$i}px); + } +} + +.flex-center { + display: flex; + align-items: center; +} \ No newline at end of file diff --git a/src/classpath/assets/utils.ts b/src/classpath/assets/utils.ts index c690d99f..95767152 100644 --- a/src/classpath/assets/utils.ts +++ b/src/classpath/assets/utils.ts @@ -2,6 +2,7 @@ // Licensed under the MIT license. import { ProjectType } from "../../utils/webview"; +import { ClasspathEntry } from "../types"; export const WEBVIEW_ID = "java.classpathConfiguration"; @@ -34,6 +35,19 @@ export function onWillSelectOutputPath() { }); } +export function onWillUpdateClassPaths(rootPaths: string[], projectTypes: ProjectType[], sourcePaths: ClasspathEntry[][], defaultOutputPaths: string[], vmInstallPaths: string[], libraries: ClasspathEntry[][]) { + vscode.postMessage({ + command: "onWillUpdateClassPaths", + rootPaths, + projectTypes, + sourcePaths, + defaultOutputPaths, + vmInstallPaths, + libraries + }); + +} + export function onWillRemoveSourcePath(sourcePaths: string[]) { vscode.postMessage({ command: "onWillRemoveSourcePath", @@ -41,29 +55,28 @@ export function onWillRemoveSourcePath(sourcePaths: string[]) { }); } -export function onWillAddSourcePath() { +export function onWillSelectFolder(type: string) { vscode.postMessage({ - command: "onWillAddSourcePath" + command: "onWillSelectFolder", + type, }); } -export function onWillChangeJdk(jdkPath: string) { +export function onWillAddSourcePathForUnmanagedFolder() { vscode.postMessage({ - command: "onWillChangeJdk", - jdkPath, + command: "onWillAddSourcePathForUnmanagedFolder" }); } -export function onWillAddReferencedLibraries() { +export function onWillAddNewJdk() { vscode.postMessage({ - command: "onWillAddReferencedLibraries" + command: "onWillAddNewJdk" }); } -export function onWillRemoveReferencedLibraries(path: string) { +export function onWillSelectLibraries() { vscode.postMessage({ - command: "onWillRemoveReferencedLibraries", - path, + command: "onWillSelectLibraries" }); } @@ -74,3 +87,28 @@ export function onClickGotoProjectConfiguration(rootUri: string, projectType: Pr projectType, }); } + +export function onWillExecuteCommand(id: string) { + vscode.postMessage({ + command: "onWillExecuteCommand", + id, + }); +} + +// TODO: better way to handle the max height calculation? +export const updateMaxHeight = () => { + let maxHeight = window.innerHeight; + const projectSelector = document.getElementById("project-selector"); + if (projectSelector) { + maxHeight -= projectSelector.getBoundingClientRect().height; + } + const footer = document.getElementById("footer"); + if (footer) { + maxHeight -= footer.getBoundingClientRect().height; + } + maxHeight -= 120; + const areas = Array.from(document.getElementsByClassName("setting-overflow-area") as HTMLCollectionOf); + for (let i = 0; i < areas.length; i++) { + areas[i].style!.maxHeight = (maxHeight <= 10 ? 10 : maxHeight) + "px"; + } +} \ No newline at end of file diff --git a/src/classpath/classpathConfigurationView.ts b/src/classpath/classpathConfigurationView.ts index 46a68658..71f9f794 100644 --- a/src/classpath/classpathConfigurationView.ts +++ b/src/classpath/classpathConfigurationView.ts @@ -5,9 +5,8 @@ import * as vscode from "vscode"; import * as path from "path"; import { getExtensionContext, getNonce } from "../utils"; import * as fse from "fs-extra"; -import { ProjectInfo, ClasspathComponent, ClasspathViewException, VmInstall } from "./types"; +import { ProjectInfo, ClasspathComponent, ClasspathViewException, VmInstall, ClasspathEntry, ClasspathEntryKind } from "./types"; import _ from "lodash"; -import minimatch from "minimatch"; import { instrumentOperation, sendError, sendInfo, setUserError } from "vscode-extension-telemetry-wrapper"; import { getProjectNameFromUri, getProjectType, isDefaultProject } from "../utils/jdt"; import { ProjectType } from "../utils/webview"; @@ -16,11 +15,10 @@ import compareVersions from "compare-versions"; let classpathConfigurationPanel: vscode.WebviewPanel | undefined; let lsApi: LanguageServerAPI | undefined; let currentProjectRoot: vscode.Uri; -const Nature_IDS: string = "org.eclipse.jdt.ls.core.natureIds" -const SOURCE_PATH_KEY: string = "org.eclipse.jdt.ls.core.sourcePaths"; +const NATURE_IDS: string = "org.eclipse.jdt.ls.core.natureIds" const OUTPUT_PATH_KEY: string = "org.eclipse.jdt.ls.core.outputPath"; const VM_LOCATION_KEY: string = "org.eclipse.jdt.ls.core.vm.location"; -const REFERENCED_LIBRARIES_KEY: string = "org.eclipse.jdt.ls.core.referencedLibraries"; +const CLASSPATH_ENTRIES_KEY: string = "org.eclipse.jdt.ls.core.classpathEntries"; const MINIMUM_JAVA_EXTENSION_VERSION: string = "0.77.0"; export async function showClasspathConfigurationPage(context: vscode.ExtensionContext): Promise { @@ -31,7 +29,7 @@ export async function showClasspathConfigurationPage(context: vscode.ExtensionCo classpathConfigurationPanel = vscode.window.createWebviewPanel( "java.classpathConfiguration", - "Classpath Configuration", + "Project Settings", vscode.ViewColumn.Active, { enableScripts: true, @@ -77,26 +75,28 @@ async function initializeWebview(context: vscode.ExtensionContext): Promise - Classpath Configuration + Project Settings @@ -241,7 +241,7 @@ const loadProjectClasspath = instrumentOperation("classpath.loadClasspath", asyn sources: classpath.sourcePaths, output: classpath.defaultOutputPath, activeVmInstallPath: classpath.jdkPath, - referencedLibraries: classpath.referenceLibraries + libraries: classpath.libraries }); } @@ -252,7 +252,7 @@ const loadProjectClasspath = instrumentOperation("classpath.loadClasspath", asyn const debounceLoadProjectClasspath = _.debounce(loadProjectClasspath, 3000 /*ms*/); -const addSourcePath = instrumentOperation("classpath.addSourcePath", async (_operationId: string, currentProjectRoot: vscode.Uri) => { +async function selectSourceFolderPath(currentProjectRoot: vscode.Uri): Promise { const sourceFolder: vscode.Uri[] | undefined = await vscode.window.showOpenDialog({ defaultUri: vscode.workspace.workspaceFolders?.[0].uri, openLabel: "Select Source Folder", @@ -273,33 +273,101 @@ const addSourcePath = instrumentOperation("classpath.addSourcePath", async (_ope if (!relativePath) { relativePath = "."; } - const sourcePaths: string[] = vscode.workspace.getConfiguration("java", currentProjectRoot).get("project.sourcePaths") || []; - if (sourcePaths.includes(relativePath)) { - vscode.window.showInformationMessage(`The path ${relativePath} has already been a source path.`); - return; - } - sourcePaths.push(relativePath); - vscode.workspace.getConfiguration("java", currentProjectRoot).update( - "project.sourcePaths", - sourcePaths, - vscode.ConfigurationTarget.Workspace, - ); - classpathConfigurationPanel?.webview.postMessage({ - command: "onDidUpdateSourceFolder", - sourcePaths, - }); + return relativePath; } + return undefined; +} + +const addSourcePathForUnmanagedFolder = instrumentOperation("classpath.addSourcePathForUnmanagedFolder", async (_operationId: string, currentProjectRoot: vscode.Uri) => { + const relativePath: string | undefined = await selectSourceFolderPath(currentProjectRoot); + if (!relativePath) { + return; + } + const sourcePaths: string[] = vscode.workspace.getConfiguration("java", currentProjectRoot).get("project.sourcePaths") || []; + if (sourcePaths.includes(relativePath)) { + vscode.window.showInformationMessage(`The path ${relativePath} has already been a source path.`); + return; + } + sourcePaths.push(relativePath); + classpathConfigurationPanel?.webview.postMessage({ + command: "onDidUpdateSourceFolder", + sourcePaths, + }); }); -const removeSourcePath = instrumentOperation("classpath.removeSourcePath", (_operationId: string, currentProjectRoot: vscode.Uri, sourcePaths: string[]) => { +const updateSourcePathsForUnmanagedFolder = instrumentOperation("classpath.updateSourcePathsForUnmanagedFolder", async (_operationId: string, currentProjectRoot: vscode.Uri, sourcePaths: string[]) => { vscode.workspace.getConfiguration("java", currentProjectRoot).update( "project.sourcePaths", sourcePaths, - vscode.ConfigurationTarget.Workspace + vscode.ConfigurationTarget.Workspace, ); }); -const setOutputPath = instrumentOperation("classpath.setOutputPath", async (operationId: string, currentProjectRoot: vscode.Uri) => { +const selectFolder = instrumentOperation("classpath.selectFolder", async (_operationId: string, currentProjectRoot: vscode.Uri, type: string) => { + const relativePath: string | undefined = await selectSourceFolderPath(currentProjectRoot); + if (!relativePath) { + return; + } + classpathConfigurationPanel?.webview.postMessage({ + command: "onDidSelectFolder", + path: relativePath, + type: type, + }); +}); + +const updateClassPaths = instrumentOperation("classpath.updateClassPaths", async (_operationId: string, rootPaths: string[], projectTypes: ProjectType[], sourcePaths: ClasspathEntry[][], defaultOutputPaths: string[], vmInstallPaths: string[], libraries: ClasspathEntry[][]) => { + classpathConfigurationPanel?.webview.postMessage({ + command: "onDidChangeLoadingState", + loading: true, + }); + + try { + const projectCount = rootPaths.length; + for (let i = 0; i < projectCount; i++) { + const currentProjectRoot: vscode.Uri = vscode.Uri.parse(rootPaths[i]); + if (projectTypes[i] === ProjectType.UnmanagedFolder) { + updateSourcePathsForUnmanagedFolder(currentProjectRoot, sourcePaths[i].map(sp => sp.path)); + setOutputPath( currentProjectRoot, defaultOutputPaths[i]); + updateUnmanagedFolderLibraries(libraries[i].map(l => l.path)); + changeJdk(currentProjectRoot, vmInstallPaths[i]); + } else { + const classpathEntries: ClasspathEntry[] = []; + classpathEntries.push(...sourcePaths[i]); + if (vmInstallPaths[i]?.length > 0) { + classpathEntries.push({ + kind: ClasspathEntryKind.Container, + path: `org.eclipse.jdt.launching.JRE_CONTAINER/${vmInstallPaths[i]}`, + }); + } + classpathEntries.push(...libraries[i]); + if (classpathEntries.length > 0) { + await vscode.commands.executeCommand( + "java.execute.workspaceCommand", + "java.project.updateClassPaths", + currentProjectRoot.toString(), + JSON.stringify({classpathEntries}), + ); + } + } + } + } catch (error) { + const err: Error = new Error(`Failed to update classpaths: ${error}`); + vscode.window.showErrorMessage(err.message, "Open Log Files").then((choice) => { + if (choice === "Open Log Files") { + vscode.commands.executeCommand("java.open.logs"); + } + }); + setUserError(err); + sendError(err); + } + + classpathConfigurationPanel?.webview.postMessage({ + command: "onDidChangeLoadingState", + loading: false, + }); +}); + +const selectOutputPath = instrumentOperation("classpath.selectOutputPath", async (_operationId: string, currentProjectRoot: vscode.Uri) => { const outputFolder: vscode.Uri[] | undefined = await vscode.window.showOpenDialog({ defaultUri: vscode.workspace.workspaceFolders?.[0].uri, openLabel: "Select Output Folder", @@ -323,23 +391,6 @@ const setOutputPath = instrumentOperation("classpath.setOutputPath", async (oper setUserError(err); throw(err); } - if ((await fse.readdir(outputFullPath)).length) { - const choice: string | undefined = await vscode.window.showInformationMessage(`The contents in ${outputFullPath} will be removed, are you sure to continue?`, "Yes", "No"); - if (choice === "Yes") { - await fse.remove(outputFullPath); - await fse.ensureDir(outputFullPath); - } else { - sendInfo(operationId, { - canceled: "Cancelled for un-empty output folder", - }); - return; - } - } - vscode.workspace.getConfiguration("java", currentProjectRoot).update( - "project.outputPath", - outputRelativePath, - vscode.ConfigurationTarget.Workspace, - ); classpathConfigurationPanel?.webview.postMessage({ command: "onDidSelectOutputPath", output: outputRelativePath, @@ -347,26 +398,83 @@ const setOutputPath = instrumentOperation("classpath.setOutputPath", async (oper } }); -const changeJdk = instrumentOperation("classpath.changeJdk", async (operationId: string, currentProjectRoot: vscode.Uri, jdkPath: string) => { +const setOutputPath = instrumentOperation("classpath.setOutputPath", async (operationId: string, currentProjectRoot: vscode.Uri, outputRelativePath: string) => { + if (vscode.workspace.getConfiguration("java", currentProjectRoot).get("project.outputPath") === outputRelativePath) { + return; + } + + const outputFullPath = path.join(currentProjectRoot.fsPath, outputRelativePath); + if ((await fse.readdir(outputFullPath)).length) { + const choice: string | undefined = await vscode.window.showInformationMessage(`The contents in ${outputFullPath} will be removed, are you sure to continue?`, "Yes", "No"); + if (choice === "Yes") { + await fse.remove(outputFullPath); + await fse.ensureDir(outputFullPath); + } else { + sendInfo(operationId, { + canceled: "Cancelled for un-empty output folder", + }); + return; + } + } + vscode.workspace.getConfiguration("java", currentProjectRoot).update( + "project.outputPath", + outputRelativePath, + vscode.ConfigurationTarget.Workspace, + ); +}); + +const addNewJdk = instrumentOperation("classpath.addNewJdk", async (operationId: string, currentProjectRoot: vscode.Uri) => { const actionResult: Record = { name: "classpath.configuration", - kind: "use-existing-jdk" + kind: "add-new-jdk" }; - if (jdkPath === "add-new-jdk") { - actionResult.kind = "add-new-jdk"; - const javaHome: vscode.Uri[] | undefined = await vscode.window.showOpenDialog({ - defaultUri: vscode.workspace.workspaceFolders?.[0].uri, - openLabel: "Select JDK Home", - canSelectFiles: false, - canSelectFolders: true, - title: "Select the installation path of the JDK" + const javaHome: vscode.Uri[] | undefined = await vscode.window.showOpenDialog({ + defaultUri: vscode.workspace.workspaceFolders?.[0].uri, + openLabel: "Select JDK Home", + canSelectFiles: false, + canSelectFolders: true, + title: "Select the installation path of the JDK" + }); + if (!javaHome) { + return; + } + + // TODO: is there a way to delay the jdk update but register this new jdk to server side? + const result: IJdkUpdateResult = await vscode.commands.executeCommand( + "java.execute.workspaceCommand", + "java.project.updateJdk", + currentProjectRoot.toString(), + javaHome[0].fsPath + ); + + actionResult.message = result.message; + actionResult.code = result.success ? "0" : "1"; + sendInfo(operationId, actionResult); + + if (result.success) { + const activeVmInstallPath = result.message; + let vmInstalls: VmInstall[] = await getVmInstallsFromLS(); + vmInstalls = vmInstalls.sort((vmA: VmInstall, vmB: VmInstall) => { + return vmA.name.localeCompare(vmB.name); }); - if (javaHome) { - jdkPath = javaHome[0].fsPath; - } else { - return; - } + classpathConfigurationPanel?.webview.postMessage({ + command: "onDidChangeJdk", + activeVmInstallPath, + vmInstalls, + }); + } else { + vscode.window.showErrorMessage(result.message); + const err: Error = new Error(result.message); + setUserError(err); + throw(err); } +}); + +const changeJdk = instrumentOperation("classpath.changeJdk", async (operationId: string, currentProjectRoot: vscode.Uri, jdkPath: string) => { + const actionResult: Record = { + name: "classpath.configuration", + kind: "use-existing-jdk" + }; const result: IJdkUpdateResult = await vscode.commands.executeCommand( "java.execute.workspaceCommand", "java.project.updateJdk", @@ -397,7 +505,15 @@ const changeJdk = instrumentOperation("classpath.changeJdk", async (operationId: } }); -const addReferencedLibraries = instrumentOperation("classpath.addReferencedLibraries", async (_operationId: string, currentProjectRoot: vscode.Uri) => { +const executeCommand = instrumentOperation("classpath.executeCommand", async (operationId: string, commandId: string) => { + await vscode.commands.executeCommand(commandId); + sendInfo(operationId, { + operationName: "classpath.executeCommand", + arg: commandId, + }); +}); + +const selectLibraries = instrumentOperation("classpath.selectLibraries", async (_operationId: string, currentProjectRoot: vscode.Uri) => { const jarFiles: vscode.Uri[] | undefined = await vscode.window.showOpenDialog({ defaultUri: vscode.workspace.workspaceFolders?.[0].uri, openLabel: "Select Jar File", @@ -415,30 +531,21 @@ const addReferencedLibraries = instrumentOperation("classpath.addReferencedLibra } return uri.fsPath; }); - addLibraryGlobs(jarPaths); classpathConfigurationPanel?.webview.postMessage({ - command: "onDidAddReferencedLibraries", - jars: jarPaths, + command: "onDidAddLibraries", + jars: jarPaths.map(jarPath => { + return { + kind: ClasspathEntryKind.Library, + path: jarPath, + }; + }), }); } }); -const removeReferencedLibrary = instrumentOperation("classpath.removeReferencedLibrary", async (_operationId: string, currentProjectRoot: vscode.Uri, removalFsPath: string) => { - if (!path.isAbsolute(removalFsPath)) { - removalFsPath = path.join(currentProjectRoot.fsPath, removalFsPath); - } +const updateUnmanagedFolderLibraries = instrumentOperation("classpath.updateUnmanagedFolderLibraries", async (_operationId: string, jarFilePaths: string[]) => { const setting = getReferencedLibrariesSetting(); - const removedPaths = _.remove(setting.include, (include) => { - if (path.isAbsolute(include)) { - return vscode.Uri.file(include).fsPath === removalFsPath; - } else { - return include === vscode.workspace.asRelativePath(removalFsPath, false); - } - }); - if (removedPaths.length === 0) { - // No duplicated item in include array, add it into the exclude field - setting.exclude = updatePatternArray(setting.exclude, vscode.workspace.asRelativePath(removalFsPath, false)); - } + setting.include = jarFilePaths; updateReferencedLibraries(setting); }); @@ -491,11 +598,10 @@ async function getVmInstallsFromLS(): Promise { async function getProjectClasspathFromLS(uri: vscode.Uri): Promise { const queryKeys: string[] = [ - Nature_IDS, - SOURCE_PATH_KEY, + NATURE_IDS, OUTPUT_PATH_KEY, VM_LOCATION_KEY, - REFERENCED_LIBRARIES_KEY + CLASSPATH_ENTRIES_KEY, ]; const response = await lsApi!.getProjectSettings( @@ -503,50 +609,83 @@ async function getProjectClasspathFromLS(uri: vscode.Uri): Promise { - const relativePath: string = path.relative(baseFsPath, p); - if (!relativePath) { - return "."; - } - return relativePath; - }).sort((srcA: string, srcB: string) => { - return srcA.localeCompare(srcB); - }); - const outputRelativePath: string = path.relative(baseFsPath, classpath.defaultOutputPath); if (!outputRelativePath.startsWith("..")) { classpath.defaultOutputPath = path.relative(baseFsPath, classpath.defaultOutputPath); } - classpath.referenceLibraries = classpath.referenceLibraries.map(p => { - const normalizedPath: string = vscode.Uri.file(p).fsPath; + classpath.libraries = classpath.libraries.map(l => { + let normalizedPath: string = vscode.Uri.file(l.path).fsPath; if (normalizedPath.startsWith(baseFsPath)) { - return path.relative(baseFsPath, normalizedPath); - } - return normalizedPath; - }).sort((libA: string, libB: string) => { - // relative paths come first - const isAbsolutePathForA: boolean = path.isAbsolute(libA); - const isAbsolutePathForB: boolean = path.isAbsolute(libB); - if (isAbsolutePathForA && !isAbsolutePathForB) { - return 1; - } else if (!isAbsolutePathForA && isAbsolutePathForB) { - return -1; - } else { - return libA.localeCompare(libB); + normalizedPath = path.relative(baseFsPath, normalizedPath); } + + return { + ...l, + path: normalizedPath, + }; }); return classpath; } +async function getSourceRoots(classpathEntries: ClasspathEntry[], baseUri: vscode.Uri): Promise { + const result: ClasspathEntry[] = []; + const baseFsPath = baseUri.fsPath; + for (const entry of classpathEntries) { + if (entry.kind !== ClasspathEntryKind.Source) { + continue; + } + + if (!await fse.pathExists(entry.path)) { + continue; + } + let relativePath: string = path.relative(baseFsPath, entry.path); + if (!relativePath) { + relativePath = "."; + } + let relativeOutputPath: string | undefined; + if (entry.output) { + relativeOutputPath = path.relative(baseFsPath, entry.output); + if (!relativeOutputPath) { + relativeOutputPath = "."; + } + } + result.push({ + kind: entry.kind, + path: relativePath, + output: relativeOutputPath, + attributes: entry.attributes, + }); + } + return result.sort((srcA: ClasspathEntry, srcB: ClasspathEntry) => { + return srcA.path.localeCompare(srcB.path); + }); +} + +function getLibraries(classpathEntries: ClasspathEntry[]): ClasspathEntry[] { + const result: ClasspathEntry[] = []; + for (const entry of classpathEntries) { + if (entry.kind === ClasspathEntryKind.Source || entry.kind === ClasspathEntryKind.Container) { + continue; + } + result.push({ + kind: entry.kind, + path: entry.path, + attributes: entry.attributes, + }); + } + + return result; +} + function getReferencedLibrariesSetting(): IReferencedLibraries { const setting = vscode.workspace.getConfiguration("java.project").get>("referencedLibraries"); const defaultSetting: IReferencedLibraries = { include: [], exclude: [], sources: {} }; @@ -569,30 +708,6 @@ function updateReferencedLibraries(libraries: IReferencedLibraries): void { vscode.workspace.getConfiguration().update("java.project.referencedLibraries", updateSetting); } -function addLibraryGlobs(libraryGlobs: string[]) { - const setting = getReferencedLibrariesSetting(); - setting.exclude = dedupAlreadyCoveredPattern(libraryGlobs, ...setting.exclude); - setting.include = updatePatternArray(setting.include, ...libraryGlobs); - updateReferencedLibraries(setting); -} - -/** - * Check if the `update` patterns are already covered by `origin` patterns and return those uncovered - */ -function dedupAlreadyCoveredPattern(origin: string[], ...update: string[]): string[] { - return update.filter((newPattern) => { - return !origin.some((originPattern) => { - return minimatch(newPattern, originPattern); - }); - }); -} - -function updatePatternArray(origin: string[], ...update: string[]): string[] { - update = dedupAlreadyCoveredPattern(origin, ...update); - origin.push(...update); - return _.uniq(origin); -} - interface LanguageServerAPI { onDidProjectsImport: vscode.Event; onDidClasspathUpdate: vscode.Event; diff --git a/src/classpath/types.ts b/src/classpath/types.ts index 36214dbe..defcbe2e 100644 --- a/src/classpath/types.ts +++ b/src/classpath/types.ts @@ -17,10 +17,25 @@ export interface VmInstall { export interface ClasspathComponent { projectType: ProjectType; - sourcePaths: string[]; + sourcePaths: ClasspathEntry[]; defaultOutputPath: string; jdkPath: string; - referenceLibraries: string[]; + libraries: ClasspathEntry[]; +} + +export interface ClasspathEntry { + kind: ClasspathEntryKind; + path: string; + output?: string; + attributes?: { [key: string]: string } +} + +export enum ClasspathEntryKind { + Library = 1, + Project = 2, + Source = 3, + Variable = 4, + Container = 5, } export enum ClasspathViewException { @@ -28,3 +43,8 @@ export enum ClasspathViewException { StaleJavaExtension = "staleJavaExtension", NoJavaProjects = "noJavaProjects", } + +export enum ProjectState { + Unloaded = "unloaded", + Loaded = "loaded", +} \ No newline at end of file