first implementation of the toolwindow for managing python packages (PY-44934)

(cherry picked from commit edeeec04fd2cd0a72e39884b7d1b9cd498e9feab)

IDEA-CR-70495

GitOrigin-RevId: d0877d3fa5a97129e4c2de9e9510042e718c636e
This commit is contained in:
Aleksei Kniazev
2021-02-15 16:05:23 +00:00
committed by intellij-monorepo-bot
parent 49553ebbc8
commit 9f75f5a242
14 changed files with 2301 additions and 1 deletions
+2
View File
@@ -20,5 +20,7 @@ if __name__ == "__main__":
publish_cmdline(reader=reader, writer=writer, argv=args)
elif command == "rstpep2html":
publish_cmdline(reader_name='pep', writer_name='pep_html', argv=args)
elif command == "rst2html_no_code":
publish_cmdline(writer_name="html", settings_overrides={'syntax_highlight': 'none'}, argv=args)
else:
publish_cmdline(writer_name=COMMANDS[command], argv=args)
@@ -73,5 +73,8 @@
<orderEntry type="module" module-name="intellij.platform.core.ui" />
<orderEntry type="module" module-name="intellij.platform.codeStyle.impl" />
<orderEntry type="library" name="fastutil-min" level="project" />
<orderEntry type="module" module-name="intellij.markdown" />
<orderEntry type="library" name="kotlinx-coroutines-jdk8" level="project" />
<orderEntry type="library" name="jcef" level="project" />
</component>
</module>
@@ -0,0 +1,21 @@
// Copyright 2000-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
window.__IntelliJTools = {};
window.onclick = e => {
if (e.target.tagName !== "A") return true;
e.stopPropagation();
e.preventDefault();
const href = e.target.href;
if (href.indexOf("#") !== -1 && !/^https?:\/\//i.test(href)) { // internal link
const elementId = href.split('#')[1];
const elementById = window.document.getElementById(elementId);
if (elementById) {
elementById.scrollIntoViewIfNeeded();
}
return;
}
if (window.__IntelliJTools.openInBrowserCallback !== undefined) {
window.__IntelliJTools.openInBrowserCallback(href);
}
};
@@ -1224,3 +1224,20 @@ command.name.create.new.package=Create New Package {0}
dialog.title.new.python.package=New Python Package
new.package.list.item.ordinary.package=Package
new.package.list.item.namespace.package=Namespace package
# Python Packages toolwindow
python.toolwindow.packages.installed.label=Installed
python.toolwindow.packages.installed.label.searched=Installed ({0} found)
python.toolwindow.packages.pypi.repo.label.searched=PyPI repository ({0} found)
python.toolwindow.packages.pypi.repo.label=PyPI repository
python.toolwindow.packages.documentation.link=Documentation
python.toolwindow.packages.no.interpreter.text=Select an interpreter to see the installed packages
python.toolwindow.packages.latest.version.label=latest
python.toolwindow.packages.install.button=Install
python.toolwindow.packages.delete.package=Delete Package
python.toolwindow.packages.search.text.placeholder=Search for more packages
python.toolwindow.packages.description.panel.placeholder=Select a package to view documentation
python.toolwindow.packages.request.failed=<html><head></head><body><p class="empty_description">Request failed.</p></body></html>
python.toolwindow.packages.no.description.placeholder=<html><head></head><body><p class="empty_description">Package author did not provide a description.</p></body></html>
python.toolwindow.packages.collecting.packages.task.title=Collecting packages
python.toolwindow.packages.rst.parsing.failed=Failed to parse the description
@@ -0,0 +1,663 @@
/*
Adapted from https://github.com/sindresorhus/github-markdown-css
The MIT License (MIT)
Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (sindresorhus.com)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
@font-face {
font-family: octicons-anchor;
src: url(data:font/woff;charset=utf-8;base64,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) format('woff');
}
body {
background-color: #3c3f41;
-webkit-text-size-adjust: 100%;
-fx-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
text-size-adjust: 100%;
color: #cccccc;
font-family: Helvetica, Arial, freesans, sans-serif;
font-size: 14px;
line-height: 1.6;
word-wrap: break-word;
}
a {
background-color: transparent;
}
a:active,
a:hover {
outline: 0;
}
strong {
font-weight: bold;
}
h1 {
font-size: 2em;
margin: 0.67em 0;
}
img {
border: 0;
}
hr {
box-sizing: content-box;
height: 0;
}
pre {
overflow: auto;
}
code,
kbd,
pre {
font-family: monospace, monospace;
font-size: 1em;
}
input {
color: inherit;
font: inherit;
margin: 0;
}
html input[disabled] {
cursor: default;
}
input {
line-height: normal;
}
input[type="checkbox"] {
box-sizing: border-box;
padding: 0;
}
table {
border-collapse: collapse;
border-spacing: 0;
}
td,
th {
padding: 0;
}
body * {
box-sizing: border-box;
}
input {
font: 13px/1.4 Helvetica, arial, nimbussansl, liberationsans, freesans, clean, sans-serif, "Segoe UI Emoji", "Segoe UI Symbol";
}
a {
color: #589df6;
text-decoration: none;
}
a:hover,
a:active {
text-decoration: underline;
}
hr {
height: 0;
margin: 15px 0;
overflow: hidden;
background: transparent;
border: 0;
border-bottom: 1px solid #2c2c2c;
}
hr:before {
display: table;
content: "";
}
hr:after {
display: table;
clear: both;
content: "";
}
h1,
h2,
h3,
h4,
h5,
h6 {
margin-top: 15px;
margin-bottom: 15px;
line-height: 1.1;
}
h1 {
font-size: 30px;
}
h2 {
font-size: 21px;
}
h3 {
font-size: 16px;
}
h4 {
font-size: 14px;
}
h5 {
font-size: 12px;
}
h6 {
font-size: 11px;
}
blockquote {
margin: 0;
}
ul,
ol {
padding: 0;
margin-top: 0;
margin-bottom: 0;
}
ol ol,
ul ol {
list-style-type: lower-roman;
}
ul ul ol,
ul ol ol,
ol ul ol,
ol ol ol {
list-style-type: lower-alpha;
}
dd {
margin-left: 0;
}
code {
font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace;
font-size: 12px;
}
pre {
margin-top: 0;
margin-bottom: 0;
font: 12px Consolas, "Liberation Mono", Menlo, Courier, monospace;
}
.octicon {
font: normal normal normal 16px/1 octicons-anchor;
display: inline-block;
text-decoration: none;
text-rendering: auto;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.octicon-link:before {
content: '\f05c';
}
.markdown-body > *:first-child {
margin-top: 0 !important;
}
.markdown-body > *:last-child {
margin-bottom: 0 !important;
}
.anchor {
position: absolute;
top: 0;
left: 0;
display: block;
padding-right: 6px;
padding-left: 30px;
margin-left: -30px;
}
.anchor:focus {
outline: none;
}
h1,
h2,
h3,
h4,
h5,
h6 {
position: relative;
margin-top: 1em;
margin-bottom: 16px;
font-weight: bold;
line-height: 1.4;
}
h1 .octicon-link,
h2 .octicon-link,
h3 .octicon-link,
h4 .octicon-link,
h5 .octicon-link,
h6 .octicon-link {
display: none;
color: #bbbbbb;
vertical-align: middle;
}
h1:hover .anchor,
h2:hover .anchor,
h3:hover .anchor,
h4:hover .anchor,
h5:hover .anchor,
h6:hover .anchor {
padding-left: 8px;
margin-left: -30px;
text-decoration: none;
}
h1:hover .anchor .octicon-link,
h2:hover .anchor .octicon-link,
h3:hover .anchor .octicon-link,
h4:hover .anchor .octicon-link,
h5:hover .anchor .octicon-link,
h6:hover .anchor .octicon-link {
display: inline-block;
}
h1 {
padding-bottom: 0.3em;
font-size: 2.25em;
line-height: 1.2;
border-bottom: 1px solid #2c2c2c;
}
h1 .anchor {
line-height: 1;
}
h2 {
padding-bottom: 0.3em;
font-size: 1.75em;
line-height: 1.225;
border-bottom: 1px solid #2c2c2c;
}
h2 .anchor {
line-height: 1;
}
h3 {
font-size: 1.5em;
line-height: 1.43;
}
h3 .anchor {
line-height: 1.2;
}
h4 {
font-size: 1.25em;
}
h4 .anchor {
line-height: 1.2;
}
h5 {
font-size: 1em;
}
h5 .anchor {
line-height: 1.1;
}
h6 {
font-size: 1em;
color: #777;
}
h6 .anchor {
line-height: 1.1;
}
p,
blockquote,
ul,
ol,
dl,
table,
pre {
margin-top: 0;
margin-bottom: 16px;
}
hr {
height: 4px;
padding: 0;
margin: 16px 0;
background-color: #2c2c2c;
border: 0 none;
}
ul,
ol {
padding-left: 2em;
}
ul ul,
ul ol,
ol ol,
ol ul {
margin-top: 0;
margin-bottom: 0;
}
li > p {
margin-top: 16px;
}
dl {
padding: 0;
}
dl dt {
padding: 0;
margin-top: 16px;
font-size: 1em;
font-style: italic;
font-weight: bold;
}
dl dd {
padding: 0 16px;
margin-bottom: 16px;
}
blockquote {
padding: 0 15px;
color: #a0a4a5;
border-left: 4px solid #626667;
}
blockquote > :first-child {
margin-top: 0;
}
blockquote > :last-child {
margin-bottom: 0;
}
table {
display: block;
width: 100%;
overflow: auto;
word-break: normal;
word-break: keep-all;
}
table th {
font-weight: bold;
}
table th,
table td {
padding: 6px 13px;
border: 1px solid #2c2c2c;
}
table tr {
background-color: #2c2c2c;
border-top: 1px solid #2c2c2c;
}
img {
max-width: 100%;
box-sizing: border-box;
}
code {
padding: 0.2em 0;
margin: 0;
font-size: 85%;
color: #dddddd;
background-color: #333333;
border-radius: 3px;
}
code:before,
code:after {
letter-spacing: -0.2em;
content: "\00a0";
}
pre > code {
padding: 0;
margin: 0;
font-size: 100%;
word-break: normal;
white-space: pre;
background: transparent;
border: 0;
}
.highlight {
margin-bottom: 16px;
}
.highlight pre,
pre {
padding: 16px;
overflow: auto;
font-size: 85%;
line-height: 1.45;
background-color: #2c2c2c;
border-radius: 3px;
}
.highlight pre {
margin-bottom: 0;
word-break: normal;
}
pre {
word-wrap: normal;
}
pre code {
display: inline;
max-width: initial;
padding: 0;
margin: 0;
overflow: initial;
line-height: inherit;
word-wrap: normal;
background-color: transparent;
border: 0;
}
pre code:before,
pre code:after {
content: normal;
}
.pl-c {
color: #969896;
}
.pl-c1,
.pl-s .pl-v {
color: #0086b3;
}
.pl-e,
.pl-en {
color: #795da3;
}
.pl-s .pl-s1,
.pl-smi {
color: #333;
}
.pl-ent {
color: #63a35c;
}
.pl-k {
color: #a71d5d;
}
.pl-pds,
.pl-s,
.pl-s .pl-pse .pl-s1,
.pl-sr,
.pl-sr .pl-cce,
.pl-sr .pl-sra,
.pl-sr .pl-sre {
color: #183691;
}
.pl-v {
color: #ed6a43;
}
.pl-id {
color: #b52a1d;
}
.pl-ii {
background-color: #b52a1d;
color: #f8f8f8;
}
.pl-sr .pl-cce {
color: #63a35c;
font-weight: bold;
}
.pl-ml {
color: #693a17;
}
.pl-mh,
.pl-mh .pl-en,
.pl-ms {
color: #1d3e81;
font-weight: bold;
}
.pl-mq {
color: #008080;
}
.pl-mi {
color: #333;
font-style: italic;
}
.pl-mb {
color: #333;
font-weight: bold;
}
.pl-md {
background-color: #ffecec;
color: #bd2c00;
}
.pl-mi1 {
background-color: #eaffea;
color: #55a532;
}
.pl-mdr {
color: #795da3;
font-weight: bold;
}
.pl-mo {
color: #1d3e81;
}
.task-list-item {
list-style-type: none;
}
.task-list-item + .task-list-item {
margin-top: 3px;
}
.task-list-item input {
margin: 0 0.35em 0.25em -0.6em;
vertical-align: middle;
}
:checked + .radio-label {
z-index: 1;
position: relative;
border-color: #4078c0;
}
span.user-del {
text-decoration: line-through;
}
::-webkit-scrollbar {
width: 12px;
height: 12px;
background-color: inherit;
}
::-webkit-scrollbar-thumb {
-webkit-border-radius: 10px;
border: 2px solid #2c2c2c;
background-color: #404040;
}
::-webkit-scrollbar-track {
width: 12px;
height: 12px;
background-color: #2c2c2c;
}
pre.code, code { background-color: #2c2c2c !important;}
@@ -0,0 +1,698 @@
/*
Adapted from https://github.com/sindresorhus/github-markdown-css
The MIT License (MIT)
Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (sindresorhus.com)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
@font-face {
font-family: octicons-anchor;
src: url(data:font/woff;charset=utf-8;base64,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) format('woff');
}
body {
background-color: white;
-webkit-text-size-adjust: 100%;
-fx-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
text-size-adjust: 100%;
color: #333;
font-family: Helvetica, Arial, freesans, sans-serif;
font-size: 16px;
line-height: 1.6;
word-wrap: break-word;
}
a {
background-color: transparent;
}
a:active,
a:hover {
outline: 0;
}
strong {
font-weight: bold;
}
h1 {
font-size: 2em;
margin: 0.67em 0;
}
img {
border: 0;
}
hr {
box-sizing: content-box;
height: 0;
}
pre {
overflow: auto;
}
code,
kbd,
pre {
font-family: monospace, monospace;
font-size: 1em;
}
input {
color: inherit;
font: inherit;
margin: 0;
}
html input[disabled] {
cursor: default;
}
input {
line-height: normal;
}
input[type="checkbox"] {
box-sizing: border-box;
padding: 0;
}
table {
border-collapse: collapse;
border-spacing: 0;
}
td,
th {
padding: 0;
}
body * {
box-sizing: border-box;
}
input {
font: 13px/1.4 Helvetica, arial, nimbussansl, liberationsans, freesans, clean, sans-serif, "Segoe UI Emoji", "Segoe UI Symbol";
}
a {
color: #4078c0;
text-decoration: none;
}
a:hover,
a:active {
text-decoration: underline;
}
hr {
height: 0;
margin: 15px 0;
overflow: hidden;
background: transparent;
border: 0;
border-bottom: 1px solid #ddd;
}
hr:before {
display: table;
content: "";
}
hr:after {
display: table;
clear: both;
content: "";
}
h1,
h2,
h3,
h4,
h5,
h6 {
margin-top: 15px;
margin-bottom: 15px;
line-height: 1.1;
}
h1 {
font-size: 30px;
}
h2 {
font-size: 21px;
}
h3 {
font-size: 16px;
}
h4 {
font-size: 14px;
}
h5 {
font-size: 12px;
}
h6 {
font-size: 11px;
}
blockquote {
margin: 0;
}
ul,
ol {
padding: 0;
margin-top: 0;
margin-bottom: 0;
}
ol ol,
ul ol {
list-style-type: lower-roman;
}
ul ul ol,
ul ol ol,
ol ul ol,
ol ol ol {
list-style-type: lower-alpha;
}
dd {
margin-left: 0;
}
code {
font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace;
font-size: 12px;
}
pre {
margin-top: 0;
margin-bottom: 0;
font: 12px Consolas, "Liberation Mono", Menlo, Courier, monospace;
}
.octicon {
font: normal normal normal 16px/1 octicons-anchor;
display: inline-block;
text-decoration: none;
text-rendering: auto;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.octicon-link:before {
content: '\f05c';
}
.markdown-body > *:first-child {
margin-top: 0 !important;
}
.markdown-body > *:last-child {
margin-bottom: 0 !important;
}
.anchor {
position: absolute;
top: 0;
left: 0;
display: block;
padding-right: 6px;
padding-left: 30px;
margin-left: -30px;
}
.anchor:focus {
outline: none;
}
h1,
h2,
h3,
h4,
h5,
h6 {
position: relative;
margin-top: 1em;
margin-bottom: 16px;
font-weight: bold;
line-height: 1.4;
}
h1 .octicon-link,
h2 .octicon-link,
h3 .octicon-link,
h4 .octicon-link,
h5 .octicon-link,
h6 .octicon-link {
display: none;
color: #000;
vertical-align: middle;
}
h1:hover .anchor,
h2:hover .anchor,
h3:hover .anchor,
h4:hover .anchor,
h5:hover .anchor,
h6:hover .anchor {
padding-left: 8px;
margin-left: -30px;
text-decoration: none;
}
h1:hover .anchor .octicon-link,
h2:hover .anchor .octicon-link,
h3:hover .anchor .octicon-link,
h4:hover .anchor .octicon-link,
h5:hover .anchor .octicon-link,
h6:hover .anchor .octicon-link {
display: inline-block;
}
h1 {
padding-bottom: 0.3em;
font-size: 2.25em;
line-height: 1.2;
border-bottom: 1px solid #eee;
}
h1 .anchor {
line-height: 1;
}
h2 {
padding-bottom: 0.3em;
font-size: 1.75em;
line-height: 1.225;
border-bottom: 1px solid #eee;
}
h2 .anchor {
line-height: 1;
}
h3 {
font-size: 1.5em;
line-height: 1.43;
}
h3 .anchor {
line-height: 1.2;
}
h4 {
font-size: 1.25em;
}
h4 .anchor {
line-height: 1.2;
}
h5 {
font-size: 1em;
}
h5 .anchor {
line-height: 1.1;
}
h6 {
font-size: 1em;
color: #777;
}
h6 .anchor {
line-height: 1.1;
}
p,
blockquote,
ul,
ol,
dl,
table,
pre {
margin-top: 0;
margin-bottom: 16px;
}
hr {
height: 4px;
padding: 0;
margin: 16px 0;
background-color: #e7e7e7;
border: 0 none;
}
ul,
ol {
padding-left: 2em;
}
ul ul,
ul ol,
ol ol,
ol ul {
margin-top: 0;
margin-bottom: 0;
}
li > p {
margin-top: 16px;
}
dl {
padding: 0;
}
dl dt {
padding: 0;
margin-top: 16px;
font-size: 1em;
font-style: italic;
font-weight: bold;
}
dl dd {
padding: 0 16px;
margin-bottom: 16px;
}
blockquote {
padding: 0 15px;
color: #777;
border-left: 4px solid #ddd;
}
blockquote > :first-child {
margin-top: 0;
}
blockquote > :last-child {
margin-bottom: 0;
}
table {
display: block;
width: 100%;
overflow: auto;
word-break: normal;
word-break: keep-all;
}
table th {
font-weight: bold;
}
table th,
table td {
padding: 6px 13px;
border: 1px solid #ddd;
}
table tr {
background-color: #fff;
border-top: 1px solid #ccc;
}
img {
max-width: 100%;
box-sizing: border-box;
}
code {
padding: 0;
padding-top: 0.2em;
padding-bottom: 0.2em;
margin: 0;
font-size: 85%;
background-color: rgba(0, 0, 0, 0.04);
border-radius: 3px;
}
code:before,
code:after {
letter-spacing: -0.2em;
content: "\00a0";
}
pre > code {
padding: 0;
margin: 0;
font-size: 100%;
word-break: normal;
white-space: pre;
background: transparent;
border: 0;
}
.highlight {
margin-bottom: 16px;
}
.highlight pre,
pre {
padding: 16px;
overflow: auto;
font-size: 85%;
line-height: 1.45;
background-color: #f7f7f7;
border-radius: 3px;
}
.highlight pre {
margin-bottom: 0;
word-break: normal;
}
pre {
word-wrap: normal;
}
pre code {
display: inline;
max-width: initial;
padding: 0;
margin: 0;
overflow: initial;
line-height: inherit;
word-wrap: normal;
background-color: transparent;
border: 0;
}
pre code:before,
pre code:after {
content: normal;
}
kbd {
display: inline-block;
padding: 3px 5px;
font-size: 11px;
line-height: 10px;
color: #555;
vertical-align: middle;
background-color: #fcfcfc;
border: solid 1px #ccc;
border-bottom-color: #bbb;
border-radius: 3px;
box-shadow: inset 0 -1px 0 #bbb;
}
.pl-c {
color: #969896;
}
.pl-c1,
.pl-s .pl-v {
color: #0086b3;
}
.pl-e,
.pl-en {
color: #795da3;
}
.pl-s .pl-s1,
.pl-smi {
color: #333;
}
.pl-ent {
color: #63a35c;
}
.pl-k {
color: #a71d5d;
}
.pl-pds,
.pl-s,
.pl-s .pl-pse .pl-s1,
.pl-sr,
.pl-sr .pl-cce,
.pl-sr .pl-sra,
.pl-sr .pl-sre {
color: #183691;
}
.pl-v {
color: #ed6a43;
}
.pl-id {
color: #b52a1d;
}
.pl-ii {
background-color: #b52a1d;
color: #f8f8f8;
}
.pl-sr .pl-cce {
color: #63a35c;
font-weight: bold;
}
.pl-ml {
color: #693a17;
}
.pl-mh,
.pl-mh .pl-en,
.pl-ms {
color: #1d3e81;
font-weight: bold;
}
.pl-mq {
color: #008080;
}
.pl-mi {
color: #333;
font-style: italic;
}
.pl-mb {
color: #333;
font-weight: bold;
}
.pl-md {
background-color: #ffecec;
color: #bd2c00;
}
.pl-mi1 {
background-color: #eaffea;
color: #55a532;
}
.pl-mdr {
color: #795da3;
font-weight: bold;
}
.pl-mo {
color: #1d3e81;
}
kbd {
display: inline-block;
padding: 3px 5px;
font: 11px Consolas, "Liberation Mono", Menlo, Courier, monospace;
line-height: 10px;
color: #555;
vertical-align: middle;
background-color: #fcfcfc;
border: solid 1px #ccc;
border-bottom-color: #bbb;
border-radius: 3px;
box-shadow: inset 0 -1px 0 #bbb;
}
.task-list-item {
list-style-type: none;
}
.task-list-item + .task-list-item {
margin-top: 3px;
}
.task-list-item input {
margin: 0 0.35em 0.25em -0.6em;
vertical-align: middle;
}
:checked + .radio-label {
z-index: 1;
position: relative;
border-color: #4078c0;
}
span.user-del {
text-decoration: line-through;
}
::-webkit-scrollbar {
width: 12px;
height: 12px;
background-color: inherit;
}
::-webkit-scrollbar-thumb {
-webkit-border-radius: 10px;
border: 2px solid #f6f6f6;
background-color: #959595;
}
::-webkit-scrollbar-track {
background-color: #f6f6f6;
}
::-webkit-scrollbar-track:vertical {
-webkit-box-shadow: -1px 0 0px #ededed;
}
::-webkit-scrollbar-track:horizontal {
-webkit-box-shadow: 0 -1px 0px #ededed;
}
pre.code, code { background-color: #eeeeee }
@@ -399,6 +399,9 @@
<notificationGroup id="Package requirements" displayType="BALLOON" isLogByDefault="false"/>
<notificationGroup id="Pipfile Watcher" displayType="STICKY_BALLOON" isLogByDefault="false"/>
<notificationGroup id="ConfiguredPythonInterpreter" displayType="BALLOON" isLogByDefault="true" bundle="messages.PyBundle" key="sdk.has.been.configured.notification.name"/>
<toolWindow id="Python Packages" anchor="bottom"
factoryClass="com.jetbrains.python.packaging.toolwindow.PyPackagesToolWindowFactory"/>
<statistics.counterUsagesCollector implementationClass="com.jetbrains.python.sdk.add.PyAddNewEnvCollector"/>
</extensions>
@@ -383,7 +383,12 @@ public class PyPIPackageUtil {
private String homePage = "";
@SerializedName("summary")
private String summary = "";
@SerializedName("description")
private String description = "";
@SerializedName("description_content_type")
private String descriptionContentType = "";
@SerializedName("project_urls")
private Map<String, String> projectUrls = Collections.emptyMap();;
@NotNull
public String getVersion() {
@@ -409,6 +414,21 @@ public class PyPIPackageUtil {
public String getSummary() {
return StringUtil.notNullize(summary);
}
@NotNull
public String getDescription() {
return StringUtil.notNullize(description);
}
@NotNull
public String getDescriptionContentType() {
return StringUtil.notNullize(descriptionContentType);
}
@NotNull
public Map<String, String> getProjectUrls() {
return ContainerUtil.notNullize(projectUrls);
}
}
@SerializedName("info")
@@ -0,0 +1,21 @@
// Copyright 2000-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.jetbrains.python.packaging.toolwindow
import com.intellij.openapi.components.service
import com.intellij.openapi.project.DumbAware
import com.intellij.openapi.project.Project
import com.intellij.openapi.wm.ToolWindow
import com.intellij.openapi.wm.ToolWindowFactory
import com.intellij.ui.content.ContentFactory
class PyPackagesToolWindowFactory : ToolWindowFactory, DumbAware {
override fun createToolWindowContent(project: Project, toolWindow: ToolWindow) {
val service = project.service<PyPackagingToolWindowService>()
val toolWindowPanel = PyPackagingToolWindowPanel(service, toolWindow)
service.initialize(toolWindowPanel)
val content = ContentFactory.SERVICE.getInstance().createContent(toolWindowPanel.component, null, false)
toolWindow.contentManager.addContent(content)
}
}
@@ -0,0 +1,97 @@
// Copyright 2000-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.jetbrains.python.packaging.toolwindow
import com.google.common.io.Resources
import com.intellij.ide.BrowserUtil
import com.intellij.ide.ui.LafManagerListener
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Disposer
import com.intellij.openapi.util.NlsSafe
import com.intellij.ui.jcef.JBCefBrowserBase
import com.intellij.ui.jcef.JBCefJSQuery
import com.intellij.ui.jcef.JCEFHtmlPanel
import com.intellij.util.ui.UIUtil
import org.cef.browser.CefBrowser
import org.cef.handler.CefLoadHandler
import org.cef.handler.CefLoadHandlerAdapter
import java.io.IOException
import java.nio.charset.StandardCharsets
import java.util.concurrent.atomic.AtomicInteger
class PyPackagingJcefHtmlPanel(project: Project) : JCEFHtmlPanel(uniqueUrl) {
private val openInBrowser = JBCefJSQuery.create(this as JBCefBrowserBase)
private val loadedStyles: MutableMap<String, String> = mutableMapOf()
private val myCefLoadHandler: CefLoadHandler
private var myLastHtml: @NlsSafe String? = null
private val jsCodeToInject: String
private val cssStyleCodeToInject: String
get() {
val styleKey = if (UIUtil.isUnderDarcula()) "python_packaging_toolwindow_darcula.css" else "python_packaging_toolwindow_default.css"
return loadedStyles.getOrPut(styleKey) {
try {
val resource = PyPackagingJcefHtmlPanel::class.java.getResource("/styles/$styleKey") ?: error("Failed to load $styleKey")
val loadedCss = Resources.toString(resource, StandardCharsets.UTF_8)
"<style>$loadedCss</style>"
}
catch (e: IOException) {
throw RuntimeException("Failed to load $styleKey", e)
}
}
}
init {
try {
val script = PyPackagingJcefHtmlPanel::class.java.getResource("/js/pkg_toolwindow_open_in_browser.js")
?: error("Failed to load js script for Python Packaging toolwindow.")
jsCodeToInject = Resources.toString(script, StandardCharsets.UTF_8)
}
catch (e: IOException) {
throw RuntimeException("Failed to load js script for Python Packaging toolwindow.", e)
}
myCefLoadHandler = object : CefLoadHandlerAdapter() {
override fun onLoadingStateChange(browser: CefBrowser, isLoading: Boolean, canGoBack: Boolean, canGoForward: Boolean) {
if (jsCodeToInject != null) {
browser.executeJavaScript(jsCodeToInject, cefBrowser.url, 0)
browser.executeJavaScript("window.__IntelliJTools.openInBrowserCallback = link => {"
+ openInBrowser.inject("link") + "}",
cefBrowser.url, 0)
}
}
}
jbCefClient.addLoadHandler(myCefLoadHandler, cefBrowser)
openInBrowser.addHandler {
if (it.isNotEmpty()) BrowserUtil.browse(it)
null
}
Disposer.register(this, openInBrowser)
project.messageBus.connect(this).subscribe(LafManagerListener.TOPIC, LafManagerListener {
if (myLastHtml != null) setHtml(myLastHtml!!)
})
}
override fun prepareHtml(html: String): String = html.replaceFirst("<head>", "<head>$cssStyleCodeToInject")
override fun setHtml(html: String) {
myLastHtml = html
super.setHtml(html)
}
override fun dispose() {
super.dispose()
jbCefClient.removeLoadHandler(myCefLoadHandler, cefBrowser)
}
companion object {
private val ourCounter = AtomicInteger(-1)
private val ourClassUrl = PyPackagingJcefHtmlPanel::class.java.getResource(
PyPackagingJcefHtmlPanel::class.java.simpleName + ".class")!!.toExternalForm()
private val uniqueUrl: String
get() = ourClassUrl + "@" + ourCounter.incrementAndGet()
}
}
@@ -0,0 +1,398 @@
// Copyright 2000-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.jetbrains.python.packaging.toolwindow
import com.intellij.icons.AllIcons
import com.intellij.ide.BrowserUtil
import com.intellij.openapi.actionSystem.*
import com.intellij.openapi.actionSystem.impl.ActionButton
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.SimpleToolWindowPanel
import com.intellij.openapi.ui.popup.JBPopupFactory
import com.intellij.openapi.ui.popup.PopupStep
import com.intellij.openapi.ui.popup.util.BaseListPopupStep
import com.intellij.openapi.util.Disposer
import com.intellij.openapi.util.NlsSafe
import com.intellij.openapi.wm.ToolWindow
import com.intellij.openapi.wm.ToolWindowManager
import com.intellij.openapi.wm.ex.ToolWindowManagerListener
import com.intellij.ui.*
import com.intellij.ui.components.*
import com.intellij.ui.jcef.JCEFHtmlPanel
import com.intellij.util.Alarm
import com.intellij.util.SingleAlarm
import com.intellij.util.ui.JBFont
import com.intellij.util.ui.JBUI
import com.intellij.util.ui.UIUtil
import com.jetbrains.python.PyBundle.message
import java.awt.*
import java.awt.event.ActionEvent
import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent
import javax.swing.*
import javax.swing.event.DocumentEvent
class PyPackagingToolWindowPanel(service: PyPackagingToolWindowService, toolWindow: ToolWindow) : SimpleToolWindowPanel(false, true) {
private val installedPackagesHeader = JLabel(message("python.toolwindow.packages.installed.label")).apply { icon = AllIcons.General.ArrowDown }
private val pypiHeaderLabel = JLabel(message("python.toolwindow.packages.pypi.repo.label")).apply { icon = AllIcons.General.ArrowDown }
private val packageNameLabel = JLabel().apply { font = JBFont.h4().asBold(); isVisible = false }
private val versionLabel = JLabel().apply { isVisible = false }
private val documentationLink = HyperlinkLabel(message("python.toolwindow.packages.documentation.link")).apply {
addHyperlinkListener { if (documentationUrl != null) BrowserUtil.browse(documentationUrl!!) }
isVisible = false
}
private val searchTextField: SearchTextField
private val searchAlarm: Alarm
private val pypiHeaderPanel: JPanel
private val installedPackagesTable: PyPackagesTable<InstalledPackage>
private val availablePackages: PyPackagesTable<DisplayablePackage>
private val installButton: JBOptionButton
private val uninstallAction: JComponent
private val progressBar: JProgressBar
private val versionSelector: JBComboBoxLabel
private val descriptionPanel: JCEFHtmlPanel
private var currentPackageInfo: PackageInfo? = null
private var documentationUrl: String? = null
// layout
private var mainPanel: JPanel? = null
private var splitter: OnePixelSplitter? = null
private val leftPanel: JScrollPane
private val rightPanel: JComponent
internal var contentVisible: Boolean
get() = mainPanel!!.isVisible
set(value) {
mainPanel!!.isVisible = value
}
private val latestText: String
get() = message("python.toolwindow.packages.latest.version.label")
init {
withEmptyText(message("python.toolwindow.packages.no.interpreter.text"))
installedPackagesTable = PyPackagesTable(PyPackagesTableModel(), service)
availablePackages = PyPackagesTable(PyPackagesTableModel(), service)
descriptionPanel = PyPackagingJcefHtmlPanel(service.project)
Disposer.register(toolWindow.disposable, descriptionPanel)
versionSelector = JBComboBoxLabel().apply {
text = latestText
isVisible = false
addMouseListener(object : MouseAdapter() {
override fun mouseClicked(e: MouseEvent?) {
val versions = listOf(latestText) + (currentPackageInfo?.availableVersions ?: emptyList())
JBPopupFactory.getInstance().createListPopup(
object : BaseListPopupStep<String>(null, versions) {
override fun onChosen(@NlsSafe selectedValue: String?, finalChoice: Boolean): PopupStep<*>? {
this@apply.text = selectedValue
return FINAL_CHOICE
}
}, 8).showUnderneathOf(this@apply)
}
})
}
// todo: add options to install with args
installButton = JBOptionButton(object : AbstractAction(message("python.toolwindow.packages.install.button")) {
override fun actionPerformed(e: ActionEvent?) {
val version = if (versionSelector.text == latestText) null else versionSelector.text
service.installSelectedPackage(version)
}
}, null).apply { isVisible = false }
uninstallAction = ActionManager.getInstance()
.createActionToolbar(ActionPlaces.TOOLWINDOW_CONTENT, DefaultActionGroup(DefaultActionGroup().apply {
add(object : AnAction(message("python.toolwindow.packages.delete.package")) {
override fun actionPerformed(e: AnActionEvent) {
service.deleteSelectedPackage()
}
})
isPopup = true
with(templatePresentation) {
putClientProperty(ActionButton.HIDE_DROPDOWN_ICON, true)
icon = AllIcons.Actions.More
}
}), true)
.component
progressBar = JProgressBar(JProgressBar.HORIZONTAL).apply {
maximumSize.width = 200
minimumSize.width = 200
preferredSize.width = 200
isVisible = false
isIndeterminate = true
}
pypiHeaderPanel = headerPanel(pypiHeaderLabel, availablePackages)
leftPanel = ScrollPaneFactory.createScrollPane(JPanel().apply {
layout = BoxLayout(this, BoxLayout.Y_AXIS)
alignmentX = LEFT_ALIGNMENT
background = UIUtil.getListBackground()
add(headerPanel(installedPackagesHeader, installedPackagesTable))
add(installedPackagesTable)
add(pypiHeaderPanel)
availablePackages.isVisible = false
add(availablePackages)
}, true)
rightPanel = borderPanel {
add(borderPanel {
border = SideBorder(JBColor.GRAY, SideBorder.BOTTOM)
preferredSize = Dimension(preferredSize.width, 50)
minimumSize = Dimension(minimumSize.width, 50)
maximumSize = Dimension(maximumSize.width, 50)
add(boxPanel {
add(Box.createHorizontalStrut(10))
add(packageNameLabel)
add(Box.createHorizontalStrut(10))
add(documentationLink)
}, BorderLayout.WEST)
add(boxPanel {
alignmentX = Component.RIGHT_ALIGNMENT
add(progressBar)
add(versionSelector)
add(versionLabel)
add(installButton)
add(uninstallAction)
add(Box.createHorizontalStrut(10))
}, BorderLayout.EAST)
}, BorderLayout.NORTH)
add(descriptionPanel.component, BorderLayout.CENTER)
}
searchTextField = object : SearchTextField(false) {
init {
preferredSize = Dimension(250, 25)
minimumSize = Dimension(250, 25)
maximumSize = Dimension(250, 25)
textEditor.border = JBUI.Borders.empty(0, 6, 0, 0)
textEditor.isOpaque = true
textEditor.emptyText.text = message("python.toolwindow.packages.search.text.placeholder")
}
override fun onFieldCleared() {
service.handleSearch("")
}
}
searchAlarm = SingleAlarm(Runnable {
installedPackagesTable.selectionModel.clearSelection()
service.handleSearch(searchTextField.text.trim())
}, 500, ModalityState.NON_MODAL, service)
searchTextField.addDocumentListener(object : DocumentAdapter() {
override fun textChanged(e: DocumentEvent) {
searchAlarm.cancelAndRequest()
}
})
initOrientation(true)
trackOrientation(service.project, service)
}
private fun initOrientation(horizontal: Boolean) {
val jbPanelWithEmptyText = JBPanelWithEmptyText()
jbPanelWithEmptyText.emptyText.text = message("python.toolwindow.packages.description.panel.placeholder")
val second = if (splitter?.secondComponent == rightPanel) rightPanel else jbPanelWithEmptyText
val proportionKey = if (horizontal) HORIZONTAL_SPLITTER_KEY else VERTICAL_SPLITTER_KEY
splitter = OnePixelSplitter(!horizontal, proportionKey, 0.3f).apply {
firstComponent = leftPanel
secondComponent = second
}
mainPanel = borderPanel {
val topToolbar = boxPanel {
border = SideBorder(UIUtil.getBoundsColor(), SideBorder.BOTTOM)
preferredSize = Dimension(preferredSize.width, 25)
minimumSize = Dimension(minimumSize.width, 25)
maximumSize = Dimension(maximumSize.width, 25)
add(searchTextField)
}
add(topToolbar, BorderLayout.NORTH)
add(splitter!!, BorderLayout.CENTER)
}
setContent(mainPanel!!)
}
private fun trackOrientation(project: Project, service: PyPackagingToolWindowService) {
project.messageBus.connect(service).subscribe(ToolWindowManagerListener.TOPIC, object : ToolWindowManagerListener {
var myHorizontal = true
override fun stateChanged(toolWindowManager: ToolWindowManager) {
val toolWindow = toolWindowManager.getToolWindow("Python Packages")
if (toolWindow == null || toolWindow.isDisposed) return
val isHorizontal = toolWindow.anchor.isHorizontal
if (myHorizontal != isHorizontal) {
myHorizontal = isHorizontal
val content = toolWindow.contentManager.contents.find { it?.component is PyPackagingToolWindowPanel }
val panel = content?.component as? PyPackagingToolWindowPanel ?: return
panel.initOrientation(myHorizontal)
}
}
})
}
fun showSearchResult(installed: List<InstalledPackage>, available: List<DisplayablePackage>, exactMatch: Int) {
with(availablePackages) {
items = available
revalidate()
repaint()
}
with(installedPackagesTable) {
items = installed
revalidate()
repaint()
}
availablePackages.isVisible = true
pypiHeaderPanel.isVisible = true
pypiHeaderLabel.icon = AllIcons.General.ArrowDown
updatePackageHeaders()
if (exactMatch != -1) ApplicationManager.getApplication().invokeLater { scrollToPackage(exactMatch) }
}
fun resetSearch(installed: List<InstalledPackage>) {
availablePackages.isVisible = false
pypiHeaderPanel.isVisible = false
with(installedPackagesTable) {
items = installed
revalidate()
repaint()
}
updatePackageHeaders()
}
fun displaySelectedPackage(selectedPackage: DisplayablePackage, packageInfo: PackageInfo) {
packageNameLabel.text = selectedPackage.name
packageNameLabel.isVisible = true
currentPackageInfo = packageInfo
if (splitter?.secondComponent != rightPanel) {
splitter!!.secondComponent = rightPanel
}
descriptionPanel.setHtml(packageInfo.description)
documentationUrl = packageInfo.documentationUrl
documentationLink.isVisible = documentationUrl != null
if (selectedPackage is InstalledPackage) {
versionLabel.text = selectedPackage.instance.version
showInstalledControls()
}
else {
showInstallableControls()
}
}
fun startProgress() {
progressBar.isVisible = true
hideInstallableControls()
hideInstalledControls()
}
fun stopProgress() {
progressBar.isVisible = false
}
fun showInstalledControls() {
hideInstallableControls()
progressBar.isVisible = false
versionLabel.isVisible = true
uninstallAction.isVisible = true
}
fun showInstallableControls() {
hideInstalledControls()
progressBar.isVisible = false
versionSelector.isVisible = true
installButton.isVisible = true
}
private fun hideInstalledControls() {
versionLabel.isVisible = false
uninstallAction.isVisible = false
}
private fun hideInstallableControls() {
installButton.isVisible = false
versionSelector.isVisible = false
}
fun packageInstalled(selectedPackage: InstalledPackage, newFiltered: List<InstalledPackage>) {
updateInstalledPackagesTable(newFiltered)
var selectedIndex: Int? = null
newFiltered.forEach { pkg ->
val existingIndex = availablePackages.items.indexOfFirst { pkg.name == it.name }
if (selectedIndex == null && pkg.name == selectedPackage.name) selectedIndex = existingIndex
if (existingIndex != -1) {
availablePackages.listModel.removeRow(existingIndex)
availablePackages.listModel.insertRow(existingIndex, pkg)
}
}
selectedIndex?.let {
availablePackages.clearSelection()
availablePackages.selectionModel.setSelectionInterval(it, it)
}
updatePackageHeaders()
}
fun packageDeleted(deletedPackage: InstallablePackage) {
val indexInstalled = installedPackagesTable.items.indexOfFirst { it.name == deletedPackage.name }
if (indexInstalled != -1) {
if (installedPackagesTable.selectedRow == indexInstalled) installedPackagesTable.selectionModel.clearSelection()
installedPackagesTable.listModel.removeRow(indexInstalled)
}
val indexAvailable = availablePackages.items.indexOfFirst { it.name == deletedPackage.name }
if (indexAvailable != -1) {
if (availablePackages.selectedRow == indexAvailable) installedPackagesTable.selectionModel.clearSelection()
availablePackages.listModel.removeRow(indexAvailable)
availablePackages.listModel.insertRow(indexAvailable, deletedPackage)
}
updatePackageHeaders()
showInstallableControls()
}
fun updateInstalledPackagesTable(newFiltered: List<InstalledPackage>) {
installedPackagesTable.listModel.addRows(newFiltered)
}
private fun scrollToPackage(exactMatch: Int) {
availablePackages.setRowSelectionInterval(exactMatch, exactMatch)
availablePackages.columnModel.selectionModel.setSelectionInterval(0, 0)
(availablePackages.selectionModel as DefaultListSelectionModel).setSelectionInterval(exactMatch, exactMatch)
val viewport = availablePackages.parent.parent as JBViewport
val rectangle = availablePackages.getCellRect(exactMatch, 0, true)
val visibleRectangle = viewport.visibleRect
viewport.scrollRectToVisible(Rectangle(rectangle.x, rectangle.y, visibleRectangle.width, visibleRectangle.height))
}
private fun updatePackageHeaders() {
if (availablePackages.isVisible) {
installedPackagesHeader.text = message("python.toolwindow.packages.installed.label.searched", installedPackagesTable.items.size)
pypiHeaderLabel.text = message("python.toolwindow.packages.pypi.repo.label.searched", availablePackages.items.size)
}
else {
installedPackagesHeader.text = message("python.toolwindow.packages.installed.label")
pypiHeaderLabel.text = message("python.toolwindow.packages.pypi.repo.label")
}
}
companion object {
private const val HORIZONTAL_SPLITTER_KEY = "Python.PackagingToolWindow.Horizontal"
private const val VERTICAL_SPLITTER_KEY = "Python.PackagingToolWindow.Vertical"
val REQUEST_FAILED_TEXT: String
get() = message("python.toolwindow.packages.request.failed")
val NO_DESCRIPTION: String
get() = message("python.toolwindow.packages.no.description.placeholder")
}
}
@@ -0,0 +1,251 @@
// Copyright 2000-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.jetbrains.python.packaging.toolwindow
import com.google.gson.Gson
import com.intellij.ProjectTopics
import com.intellij.openapi.Disposable
import com.intellij.openapi.application.AppUIExecutor
import com.intellij.openapi.application.impl.coroutineDispatchingContext
import com.intellij.openapi.components.Service
import com.intellij.openapi.diagnostic.thisLogger
import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.openapi.progress.ProgressManager
import com.intellij.openapi.progress.Task
import com.intellij.openapi.project.Project
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.roots.ModuleRootEvent
import com.intellij.openapi.roots.ModuleRootListener
import com.intellij.openapi.util.text.StringUtil
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.util.io.HttpRequests
import com.intellij.webcore.packaging.PackageManagementService
import com.intellij.webcore.packaging.RepoPackage
import com.jetbrains.python.PyBundle.*
import com.jetbrains.python.PythonHelper
import com.jetbrains.python.packaging.*
import com.jetbrains.python.packaging.PyPackageVersionComparator.STR_COMPARATOR
import com.jetbrains.python.sdk.PySdkUtil
import com.jetbrains.python.sdk.pythonSdk
import com.jetbrains.python.statistics.modules
import kotlinx.coroutines.*
import org.intellij.plugins.markdown.ui.preview.html.MarkdownUtil
@Service
class PyPackagingToolWindowService(val project: Project) : Disposable {
private lateinit var toolWindowPanel: PyPackagingToolWindowPanel
private var installedPackages: List<InstalledPackage> = emptyList()
private var currentSdk: Sdk? = null
private var selectedPackage: DisplayablePackage? = null
private var selectedInfo: PackageInfo? = null
private var currentJob: Job? = null
private var searchJob: Job? = null
private var currentQuery: String = ""
private val gson = Gson()
fun initialize(toolWindowPanel: PyPackagingToolWindowPanel) {
this.toolWindowPanel = toolWindowPanel
initForSdk(project.modules.firstOrNull()?.pythonSdk)
subscribeToChanges()
}
fun packageSelected(pkg: DisplayablePackage) {
currentJob?.cancel()
selectedPackage = pkg
currentJob = GlobalScope.launch(Dispatchers.Default) {
val response = fetchPackageInfo(selectedPackage!!.name)
if (response != null) {
val packageDetails = gson.fromJson(response, PyPIPackageUtil.PackageDetails::class.java)
selectedInfo = with(packageDetails.info) {
val renderedDescription = when {
description.isNotEmpty() -> convertToHTML(descriptionContentType, description)
summary.isNotEmpty() -> wrapHtml(summary)
else -> PyPackagingToolWindowPanel.NO_DESCRIPTION
}
PackageInfo(projectUrls["Documentation"],
renderedDescription,
packageDetails.releases.sortedWith(STR_COMPARATOR.reversed()))
}
}
else {
selectedInfo = EMPTY_INFO
}
if (isActive) {
withContext(AppUIExecutor.onUiThread().coroutineDispatchingContext()) {
toolWindowPanel.displaySelectedPackage(selectedPackage!!, selectedInfo!!)
}
}
}
}
fun handleSearch(query: String) {
currentQuery = query
if (query.isNotEmpty()) {
searchJob?.cancel()
searchJob = GlobalScope.launch(Dispatchers.Default) {
val installed = installedPackages.filter { StringUtil.containsIgnoreCase(it.name, query) }
val pypi = PyPIPackageCache.getInstance().packageNames.asSequence()
.filter { StringUtil.containsIgnoreCase(it, query) }
.map { pkg -> installed.find { it.name == pkg } ?: InstallablePackage (pkg) }
.toList()
val exactMatch = pypi.indexOfFirst { StringUtil.equalsIgnoreCase(query, it.name) }
if (isActive) {
withContext(AppUIExecutor.onUiThread().coroutineDispatchingContext()) {
toolWindowPanel.showSearchResult(installed, pypi, exactMatch)
}
}
}
}
else {
toolWindowPanel.resetSearch(installedPackages)
}
}
fun installSelectedPackage(version: String?) {
val toInstall = selectedPackage as? InstallablePackage ?: return
val managementService = PyPackageManagers.getInstance().getManagementService(project, currentSdk)
val listener = object : PackageManagementService.Listener {
override fun operationStarted(packageName: String?) {
toolWindowPanel.startProgress()
}
override fun operationFinished(packageName: String?, errorDescription: PackageManagementService.ErrorDescription?) {
toolWindowPanel.stopProgress()
collectInstalledPackages { newPackages ->
val newFiltered = newPackages.let { packages ->
if (currentQuery.isNotEmpty()) packages.filter { StringUtil.containsIgnoreCase(it.name, currentQuery) } else packages
}
if (toInstall.name == selectedPackage?.name) {
val installed = installedPackages.find { it.name == toInstall.name }
if (installed == null) {
toolWindowPanel.showInstallableControls()
} else {
selectedPackage = installed
toolWindowPanel.packageInstalled(installed, newFiltered)
}
}
else {
toolWindowPanel.updateInstalledPackagesTable(newFiltered)
}
}
}
}
managementService.installPackage(RepoPackage(toInstall.name, null), version, false, null, listener, false)
}
fun deleteSelectedPackage() {
val packageToDelete = selectedPackage as? InstalledPackage ?: return
val managementService = PyPackageManagers.getInstance().getManagementService(project, currentSdk)
val listener = object : PackageManagementService.Listener {
override fun operationStarted(packageName: String?) {
toolWindowPanel.startProgress()
}
override fun operationFinished(packageName: String?, errorDescription: PackageManagementService.ErrorDescription?) {
toolWindowPanel.stopProgress()
collectInstalledPackages {
if (packageToDelete.name == selectedPackage?.name) {
val stillInstalled = installedPackages.find { it.name == packageToDelete.name }
if (stillInstalled != null) {
toolWindowPanel.showInstalledControls()
// todo: add toolwindow notification and remove balloons from PackageManagementService
}
else {
val installablePackage = InstallablePackage(packageToDelete.name)
selectedPackage = installablePackage
toolWindowPanel.packageDeleted(installablePackage)
}
}
}
}
}
managementService.uninstallPackages(listOf(packageToDelete.instance), listener)
}
private fun initForSdk(sdk: Sdk?) {
currentSdk = sdk
if (currentSdk != null) {
collectInstalledPackages(resetSearch = true)
}
toolWindowPanel.contentVisible = currentSdk != null
}
private fun subscribeToChanges() {
val connection = project.messageBus.connect(this)
connection.subscribe(PyPackageManager.PACKAGE_MANAGER_TOPIC, PyPackageManager.Listener {
if (currentSdk == it) collectInstalledPackages(resetSearch = currentQuery.isEmpty())
})
connection.subscribe(ProjectTopics.PROJECT_ROOTS, object : ModuleRootListener {
override fun rootsChanged(event: ModuleRootEvent) {
initForSdk(project.modules.firstOrNull()?.pythonSdk)
}
})
}
private fun collectInstalledPackages(resetSearch: Boolean = false, callback: ((List<InstalledPackage>) -> Unit)? = null) {
val task = object : Task.Backgroundable(project, message("python.toolwindow.packages.collecting.packages.task.title")) {
val sdk = currentSdk!!
val previouslyInstalled = installedPackages
var packages: List<InstalledPackage>? = null
var newPackages: List<InstalledPackage>? = null
override fun run(indicator: ProgressIndicator) {
val currentlyInstalled = previouslyInstalled.mapTo(HashSet()) { it.name }
packages = PyPackageManagers.getInstance().forSdk(sdk).refreshAndGetPackages(false).map { InstalledPackage(it) }
newPackages = packages?.filter { it.name !in currentlyInstalled }
}
override fun onSuccess() {
installedPackages = packages ?: error("No installed packages found")
if (resetSearch) toolWindowPanel.resetSearch(installedPackages)
callback?.invoke(newPackages ?: emptyList())
}
}
ProgressManager.getInstance().run(task)
}
private suspend fun fetchPackageInfo(packageName: String): String? = withContext(Dispatchers.IO) {
val result = runCatching {
HttpRequests.request("https://pypi.org/pypi/${packageName}/json").readTimeout(3000).readString()
}
if (result.isFailure) thisLogger().debug("Request failed for package $packageName")
result.getOrNull()
}
private fun convertToHTML(contentType: String, description: String): String {
return when (contentType) {
"text/markdown" -> markdownToHtml(description, currentSdk!!.homeDirectory!!, project)
"text/x-rst", "" -> rstToHtml(description, currentSdk!!)
else -> description
}
}
private fun rstToHtml(text: String, sdk: Sdk): String {
val commandLine = PythonHelper.REST_RUNNER.newCommandLine(sdk, listOf("rst2html_no_code"))
val output = PySdkUtil.getProcessOutput(commandLine, sdk.homeDirectory!!.parent.path, null, 5000,
text.toByteArray(Charsets.UTF_8), false)
return when {
output.checkSuccess(thisLogger()) -> output.stdout
else -> wrapHtml("<p>${message("python.toolwindow.packages.rst.parsing.failed")}</p>")
}
}
private fun markdownToHtml(text: String, homeDir: VirtualFile, project: Project): String {
return wrapHtml(MarkdownUtil.generateMarkdownHtml(homeDir, text, project))
}
override fun dispose() {
currentJob?.cancel()
searchJob?.cancel()
}
private fun wrapHtml(html: String): String = "<html><head></head><body><p>$html</p></body></html>"
companion object {
private val EMPTY_INFO = PackageInfo(null, PyPackagingToolWindowPanel.REQUEST_FAILED_TEXT, emptyList())
}
}
@@ -0,0 +1,10 @@
// Copyright 2000-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.jetbrains.python.packaging.toolwindow
import com.intellij.openapi.util.NlsSafe
import com.jetbrains.python.packaging.PyPackage
sealed class DisplayablePackage(@NlsSafe val name: String)
class InstalledPackage(val instance: PyPackage) : DisplayablePackage(instance.name)
class InstallablePackage(name: String) : DisplayablePackage(name)
class PackageInfo(val documentationUrl: String?, @NlsSafe val description: String, val availableVersions: List<String>)
@@ -0,0 +1,96 @@
// Copyright 2000-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.jetbrains.python.packaging.toolwindow
import com.intellij.icons.AllIcons
import com.intellij.ui.SideBorder
import com.intellij.ui.table.JBTable
import com.intellij.util.ui.ListTableModel
import com.intellij.util.ui.UIUtil
import java.awt.BorderLayout
import java.awt.Dimension
import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent
import javax.swing.*
import javax.swing.border.EmptyBorder
class PyPackagesTable<T : DisplayablePackage>(model: ListTableModel<T>, service: PyPackagingToolWindowService) : JBTable(model) {
var lastSelectedRow = -1
init {
setShowGrid(false)
setSelectionMode(ListSelectionModel.SINGLE_SELECTION)
rowHeight = 20
selectionModel.addListSelectionListener {
if (selectedRow != -1 && selectedRow != lastSelectedRow) {
lastSelectedRow = selectedRow
val pkg = model.items[selectedRow]
service.packageSelected(pkg)
}
}
}
override fun clearSelection() {
lastSelectedRow = -1
super.clearSelection()
}
@Suppress("UNCHECKED_CAST")
val listModel: ListTableModel<T>
get() = model as ListTableModel<T>
var items: List<T>
get() = listModel.items
set(value) { listModel.items = value }
}
class PyPackagesTableModel<T : DisplayablePackage> : ListTableModel<T>() {
override fun isCellEditable(rowIndex: Int, columnIndex: Int): Boolean = false
override fun getColumnCount(): Int = 2
override fun getColumnName(column: Int): String = column.toString()
override fun getColumnClass(columnIndex: Int): Class<*> {
if (columnIndex == 0) return String::class.java
if (columnIndex == 1) return Number::class.java
return Any::class.java
}
override fun getValueAt(rowIndex: Int, columnIndex: Int): Any? {
val item = items[rowIndex]
if (columnIndex == 0) return item.name
if (columnIndex == 1 && item is InstalledPackage) return item.instance.version
return null
}
}
fun boxPanel(init: JPanel.() -> Unit) = object : JPanel() {
init {
layout = BoxLayout(this, BoxLayout.X_AXIS)
alignmentX = LEFT_ALIGNMENT
init()
}
}
fun borderPanel(init: JPanel.() -> Unit) = object : JPanel() {
init {
layout = BorderLayout(0, 0)
init()
}
}
fun headerPanel(label: JLabel, component: JComponent) = object : JPanel() {
init {
background = UIUtil.getControlColor()
layout = BorderLayout()
border = BorderFactory.createCompoundBorder(SideBorder(UIUtil.getBoundsColor(), SideBorder.BOTTOM), EmptyBorder(0, 5, 0, 5))
preferredSize = Dimension(preferredSize.width, 25)
minimumSize = Dimension(minimumSize.width, 25)
maximumSize = Dimension(maximumSize.width, 25)
add(label, BorderLayout.WEST)
addMouseListener(object : MouseAdapter() {
override fun mouseClicked(e: MouseEvent?) {
component.isVisible = !component.isVisible
label.icon = if (component.isVisible) AllIcons.General.ArrowDown else AllIcons.General.ArrowRight
}
})
}
}