mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
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:
committed by
intellij-monorepo-bot
parent
49553ebbc8
commit
9f75f5a242
@@ -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,d09GRgABAAAAAAYcAA0AAAAACjQAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAABGRlRNAAABMAAAABwAAAAca8vGTk9TLzIAAAFMAAAARAAAAFZG1VHVY21hcAAAAZAAAAA+AAABQgAP9AdjdnQgAAAB0AAAAAQAAAAEACICiGdhc3AAAAHUAAAACAAAAAj//wADZ2x5ZgAAAdwAAADRAAABEKyikaNoZWFkAAACsAAAAC0AAAA2AtXoA2hoZWEAAALgAAAAHAAAACQHngNFaG10eAAAAvwAAAAQAAAAEAwAACJsb2NhAAADDAAAAAoAAAAKALIAVG1heHAAAAMYAAAAHwAAACABEAB2bmFtZQAAAzgAAALBAAAFu3I9x/Nwb3N0AAAF/AAAAB0AAAAvaoFvbwAAAAEAAAAAzBdyYwAAAADP2IQvAAAAAM/bz7t4nGNgZGFgnMDAysDB1Ml0hoGBoR9CM75mMGLkYGBgYmBlZsAKAtJcUxgcPsR8iGF2+O/AEMPsznAYKMwIkgMA5REMOXicY2BgYGaAYBkGRgYQsAHyGMF8FgYFIM0ChED+h5j//yEk/3KoSgZGNgYYk4GRCUgwMaACRoZhDwCs7QgGAAAAIgKIAAAAAf//AAJ4nHWMMQrCQBBF/0zWrCCIKUQsTDCL2EXMohYGSSmorScInsRGL2DOYJe0Ntp7BK+gJ1BxF1stZvjz/v8DRghQzEc4kIgKwiAppcA9LtzKLSkdNhKFY3HF4lK69ExKslx7Xa+vPRVS43G98vG1DnkDMIBUgFN0MDXflU8tbaZOUkXUH0+U27RoRpOIyCKjbMCVejwypzJJG4jIwb43rfl6wbwanocrJm9XFYfskuVC5K/TPyczNU7b84CXcbxks1Un6H6tLH9vf2LRnn8Ax7A5WQAAAHicY2BkYGAA4teL1+yI57f5ysDNwgAC529f0kOmWRiYVgEpDgYmEA8AUzEKsQAAAHicY2BkYGB2+O/AEMPCAAJAkpEBFbAAADgKAe0EAAAiAAAAAAQAAAAEAAAAAAAAKgAqACoAiAAAeJxjYGRgYGBhsGFgYgABEMkFhAwM/xn0QAIAD6YBhwB4nI1Ty07cMBS9QwKlQapQW3VXySvEqDCZGbGaHULiIQ1FKgjWMxknMfLEke2A+IJu+wntrt/QbVf9gG75jK577Lg8K1qQPCfnnnt8fX1NRC/pmjrk/zprC+8D7tBy9DHgBXoWfQ44Av8t4Bj4Z8CLtBL9CniJluPXASf0Lm4CXqFX8Q84dOLnMB17N4c7tBo1AS/Qi+hTwBH4rwHHwN8DXqQ30XXAS7QaLwSc0Gn8NuAVWou/gFmnjLrEaEh9GmDdDGgL3B4JsrRPDU2hTOiMSuJUIdKQQayiAth69r6akSSFqIJuA19TrzCIaY8sIoxyrNIrL//pw7A2iMygkX5vDj+G+kuoLdX4GlGK/8Lnlz6/h9MpmoO9rafrz7ILXEHHaAx95s9lsI7AHNMBWEZHULnfAXwG9/ZqdzLI08iuwRloXE8kfhXYAvE23+23DU3t626rbs8/8adv+9DWknsHp3E17oCf+Z48rvEQNZ78paYM38qfk3v/u3l3u3GXN2Dmvmvpf1Srwk3pB/VSsp512bA/GG5i2WJ7wu430yQ5K3nFGiOqgtmSB5pJVSizwaacmUZzZhXLlZTq8qGGFY2YcSkqbth6aW1tRmlaCFs2016m5qn36SbJrqosG4uMV4aP2PHBmB3tjtmgN2izkGQyLWprekbIntJFing32a5rKWCN/SdSoga45EJykyQ7asZvHQ8PTm6cslIpwyeyjbVltNikc2HTR7YKh9LBl9DADC0U/jLcBZDKrMhUBfQBvXRzLtFtjU9eNHKin0x5InTqb8lNpfKv1s1xHzTXRqgKzek/mb7nB8RZTCDhGEX3kK/8Q75AmUM/eLkfA+0Hi908Kx4eNsMgudg5GLdRD7a84npi+YxNr5i5KIbW5izXas7cHXIMAau1OueZhfj+cOcP3P8MNIWLyYOBuxL6DRylJ4cAAAB4nGNgYoAALjDJyIAOWMCiTIxMLDmZedkABtIBygAAAA==) 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
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user