From 8adaaef9561cbac406fac1761ba83a59ce40372d Mon Sep 17 00:00:00 2001 From: Vladimir Krivosheev Date: Mon, 2 Feb 2015 12:51:02 +0100 Subject: [PATCH 1/3] document recently added REST API (diff), add AboutHttpService --- build/build.iml | 4 +- build/restApiDocGenerator/.gitignore | 1 + build/restApiDocGenerator/apidoc.json | 5 + build/restApiDocGenerator/gulpfile.js | 15 +++ build/restApiDocGenerator/package.json | 7 ++ .../org/jetbrains/ide/AboutHttpService.coffee | 43 +++++++ .../org/jetbrains/ide/AboutHttpService.java | 115 ++++++++++++++++++ .../org/jetbrains/ide/DiffHttpService.coffee | 18 +++ .../org/jetbrains/ide/DiffHttpService.java | 55 ++++++--- .../src/org/jetbrains/ide/RestService.java | 26 +++- .../src/META-INF/PlatformExtensions.xml | 1 + 11 files changed, 271 insertions(+), 19 deletions(-) create mode 100644 build/restApiDocGenerator/.gitignore create mode 100644 build/restApiDocGenerator/apidoc.json create mode 100644 build/restApiDocGenerator/gulpfile.js create mode 100644 build/restApiDocGenerator/package.json create mode 100644 platform/platform-impl/src/org/jetbrains/ide/AboutHttpService.coffee create mode 100644 platform/platform-impl/src/org/jetbrains/ide/AboutHttpService.java create mode 100644 platform/platform-impl/src/org/jetbrains/ide/DiffHttpService.coffee diff --git a/build/build.iml b/build/build.iml index 7486cf38b75d..56fe67fc3588 100644 --- a/build/build.iml +++ b/build/build.iml @@ -5,6 +5,7 @@ + @@ -35,5 +36,4 @@ - - + \ No newline at end of file diff --git a/build/restApiDocGenerator/.gitignore b/build/restApiDocGenerator/.gitignore new file mode 100644 index 000000000000..40b878db5b1c --- /dev/null +++ b/build/restApiDocGenerator/.gitignore @@ -0,0 +1 @@ +node_modules/ \ No newline at end of file diff --git a/build/restApiDocGenerator/apidoc.json b/build/restApiDocGenerator/apidoc.json new file mode 100644 index 000000000000..bf3fd87a4282 --- /dev/null +++ b/build/restApiDocGenerator/apidoc.json @@ -0,0 +1,5 @@ +{ + "name": "IntelliJ Platform REST API", + "version": "140.2110", + "url" : "http://localhost:63342/rest" +} \ No newline at end of file diff --git a/build/restApiDocGenerator/gulpfile.js b/build/restApiDocGenerator/gulpfile.js new file mode 100644 index 000000000000..e583e04c7c23 --- /dev/null +++ b/build/restApiDocGenerator/gulpfile.js @@ -0,0 +1,15 @@ +var gulp = require('gulp') +var apidoc = require('gulp-apidoc') +var path = require('path') + +var sources = path.normalize("../../platform/platform-impl/src/org/jetbrains/ide") + +gulp.task('apidoc', function () { + apidoc.exec({src: sources, dest: (process.env.HOME || process.env.HOMEPATH || process.env.USERPROFILE) + "/idea-rest-api"}) +}) + +gulp.task('default', ['apidoc']) + +gulp.task('watch', function() { + gulp.watch(sources + "/**/*.{clj,coffee,cs,dart,erl,go,java,js,php,py,rb,ts,pm}", ['apidoc']) +}) \ No newline at end of file diff --git a/build/restApiDocGenerator/package.json b/build/restApiDocGenerator/package.json new file mode 100644 index 000000000000..eb04a6f6572f --- /dev/null +++ b/build/restApiDocGenerator/package.json @@ -0,0 +1,7 @@ +{ + "name": "restApiDocGenerator", + "devDependencies": { + "gulp": "^3.8.10", + "gulp-apidoc": "^0.1.2" + } +} diff --git a/platform/platform-impl/src/org/jetbrains/ide/AboutHttpService.coffee b/platform/platform-impl/src/org/jetbrains/ide/AboutHttpService.coffee new file mode 100644 index 000000000000..1c7a257611bf --- /dev/null +++ b/platform/platform-impl/src/org/jetbrains/ide/AboutHttpService.coffee @@ -0,0 +1,43 @@ +### + @apiDefine SuccessExample + + @apiSuccessExample {json} Success-Response: +{ + "name": "WebStorm 140.SNAPSHOT", + "productName": "WebStorm", + "baselineVersion": 140 +} +### + +### + @apiDefine SuccessExampleWithRegisteredFileTypes + + @apiSuccessExample {json} Success-Response (with registeredFileTypes): +{ + "name": "WebStorm 140.SNAPSHOT", + "productName": "WebStorm", + "baselineVersion": 140, + "registeredFileTypes": [ + { + "name": "HTML", + "description": "HTML files", + "isBinary": false + }, + { + "name": "XHTML", + "description": "XHTML files", + "isBinary": false + }, + { + "name": "DTD", + "description": "XML Document Type Definition", + "isBinary": false + }, + { + "name": "XML", + "description": "XML files", + "isBinary": false + } + ] +} +### \ No newline at end of file diff --git a/platform/platform-impl/src/org/jetbrains/ide/AboutHttpService.java b/platform/platform-impl/src/org/jetbrains/ide/AboutHttpService.java new file mode 100644 index 000000000000..a2ad88973999 --- /dev/null +++ b/platform/platform-impl/src/org/jetbrains/ide/AboutHttpService.java @@ -0,0 +1,115 @@ +/* + * Copyright 2000-2015 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.ide; + +import com.google.gson.stream.JsonWriter; +import com.intellij.openapi.application.ApplicationInfo; +import com.intellij.openapi.application.ApplicationNamesInfo; +import com.intellij.openapi.application.ex.ApplicationInfoEx; +import com.intellij.openapi.fileTypes.FileType; +import com.intellij.openapi.fileTypes.FileTypeRegistry; +import com.intellij.openapi.util.BuildNumber; +import com.intellij.openapi.util.io.BufferExposingByteArrayOutputStream; +import com.intellij.openapi.vfs.CharsetToolkit; +import com.intellij.util.PlatformUtils; +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.http.*; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.io.Responses; + +import java.io.IOException; +import java.io.OutputStreamWriter; + +/** + * @api {get} /about The application info + * @apiName about + * @apiGroup Platform + * + * @apiParam {Boolean} [registeredFileTypes=false] Whether to include the list of registered file types. + * + * @apiSuccess {String} name The full application name. + * @apiSuccess {String} productName The product name. + * @apiSuccess {String} baselineVersion The baseline version. + * @apiSuccess {String} [buildNumber] The build number. + * + * @apiSuccess {Object[]} registeredFileTypes The list of registered file types. + * @apiSuccess {String} registeredFileTypes.name The name of file type. + * @apiSuccess {String} registeredFileTypes.description The user-readable description of the file type. + * @apiSuccess {Boolean} registeredFileTypes.isBinary Whether files of the specified type contain binary data. + * + * * @apiExample Request-Example: + * /rest/about?registeredFileTypes + * + * @apiUse SuccessExample + * @apiUse SuccessExampleWithRegisteredFileTypes + */ +class AboutHttpService extends RestService { + @NotNull + @Override + protected String getServiceName() { + return "about"; + } + + @Override + protected boolean isMethodSupported(@NotNull HttpMethod method) { + return method == HttpMethod.GET; + } + + @Nullable + @Override + public String execute(@NotNull QueryStringDecoder urlDecoder, @NotNull FullHttpRequest request, @NotNull ChannelHandlerContext context) throws IOException { + BuildNumber build = ApplicationInfo.getInstance().getBuild(); + @SuppressWarnings("IOResourceOpenedButNotSafelyClosed") + BufferExposingByteArrayOutputStream byteOut = new BufferExposingByteArrayOutputStream(); + JsonWriter writer = new JsonWriter(new OutputStreamWriter(byteOut, CharsetToolkit.UTF8_CHARSET)); + writer.beginObject(); + + String appName = ApplicationInfoEx.getInstanceEx().getFullApplicationName(); + if (!PlatformUtils.isIdeaUltimate()) { + String productName = ApplicationNamesInfo.getInstance().getProductName(); + appName = appName.replace(productName + " (" + productName + ")", productName); + if (appName.startsWith("JetBrains ")) { + appName = appName.substring("JetBrains ".length()); + } + } + + writer.name("name").value(appName); + writer.name("productName").value(ApplicationNamesInfo.getInstance().getProductName()); + writer.name("baselineVersion").value(build.getBaselineVersion()); + if (build.getBuildNumber() != Integer.MAX_VALUE) { + writer.name("buildNumber").value(build.getBuildNumber()); + } + + if (getBooleanParameter("registeredFileTypes", urlDecoder)) { + writer.name("registeredFileTypes").beginArray(); + for (FileType fileType : FileTypeRegistry.getInstance().getRegisteredFileTypes()) { + writer.beginObject(); + writer.name("name").value(fileType.getName()); + writer.name("description").value(fileType.getDescription()); + writer.name("isBinary").value(fileType.isBinary()); + writer.endObject(); + } + writer.endArray(); + } + + writer.endObject(); + writer.close(); + Responses.send(new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, Unpooled.wrappedBuffer(byteOut.getInternalBuffer(), 0, byteOut.size())), context.channel(), request); + return null; + } +} diff --git a/platform/platform-impl/src/org/jetbrains/ide/DiffHttpService.coffee b/platform/platform-impl/src/org/jetbrains/ide/DiffHttpService.coffee new file mode 100644 index 000000000000..cbd9111c0f1d --- /dev/null +++ b/platform/platform-impl/src/org/jetbrains/ide/DiffHttpService.coffee @@ -0,0 +1,18 @@ +### + @apiDefine DiffRequestExample + + @apiExample {json} Request-Example: +{ + "fileType": "JSON", + "contents": [ + { + "title": "Actual", + "content": "foo" + }, + { + "title": "Expected", + "content": "bar" + } + ] +} +### \ No newline at end of file diff --git a/platform/platform-impl/src/org/jetbrains/ide/DiffHttpService.java b/platform/platform-impl/src/org/jetbrains/ide/DiffHttpService.java index e91418e6103e..73d64bd4c694 100644 --- a/platform/platform-impl/src/org/jetbrains/ide/DiffHttpService.java +++ b/platform/platform-impl/src/org/jetbrains/ide/DiffHttpService.java @@ -38,34 +38,57 @@ import java.io.IOException; import java.util.ArrayList; import java.util.List; +/** + * @api {post} /diff The differences between contents + * @apiName diff + * @apiGroup Platform + * + * @apiParam (properties) {String} [fileType] The file type name of the contents (see FileType.getName()). + * You can get registered file types using /rest/about?registeredFileTypes request. + * @apiParam (properties) {String} [windowTitle=Diff Service] The title of the diff window. + * @apiParam (properties) {Boolean} [focused=true] Whether to focus project window. + * + * @apiParam (properties) {Object[]{2..}} contents The list of the contents to diff. + * @apiParam (properties) {String} [contents.title] The title of the content. + * @apiParam (properties) {String} [contents.fileType] The file type name of the content. + * @apiParam (properties) {String} contents.content The data of the content. + * + * @apiUse DiffRequestExample + */ final class DiffHttpService extends RestService { - @Override - protected boolean isMethodSupported(@NotNull HttpMethod method) { - return method == HttpMethod.POST; - } - @NotNull @Override protected String getServiceName() { return "diff"; } + @Override + protected boolean isMethodSupported(@NotNull HttpMethod method) { + return method == HttpMethod.POST; + } + @Override @Nullable public String execute(@NotNull QueryStringDecoder urlDecoder, @NotNull FullHttpRequest request, @NotNull ChannelHandlerContext context) throws IOException { final List contents = new ArrayList(); final List titles = new ArrayList(); - + boolean focused = true; + String windowTitle = null; JsonReader reader = createJsonReader(request); if (reader.hasNext()) { String fileType = null; - boolean reformat; reader.beginObject(); while (reader.hasNext()) { String name = reader.nextName(); - if (name.equals("fileTypeName")) { + if (name.equals("fileType")) { fileType = reader.nextString(); } + else if (name.equals("focused")) { + focused = reader.nextBoolean(); + } + else if (name.equals("windowTitle")) { + windowTitle = StringUtil.nullize(reader.nextString(), true); + } else if (name.equals("contents")) { String error = readContent(reader, contents, titles, fileType); if (error != null) { @@ -89,6 +112,8 @@ final class DiffHttpService extends RestService { return "No opened project, please open any project"; } + final boolean finalFocused = focused; + final String finalWindowTitle = windowTitle; ApplicationManager.getApplication().invokeLater(new Runnable() { @Override public void run() { @@ -106,21 +131,23 @@ final class DiffHttpService extends RestService { @Override public String getWindowTitle() { - return "Diff Service"; + return StringUtil.notNullize(finalWindowTitle, "Diff Service"); } }); - ProjectUtil.focusProjectWindow(project, true); + if (finalFocused) { + ProjectUtil.focusProjectWindow(project, true); + } } }, project.getDisposed()); return null; } @Nullable - private static String readContent(@NotNull JsonReader reader, @NotNull List contents, @NotNull List titles, @Nullable String defaultFileTypeName) throws IOException { + private static String readContent(@NotNull JsonReader reader, @NotNull List contents, @NotNull List titles, @Nullable String defaultfileType) throws IOException { FileTypeRegistry fileTypeRegistry = FileTypeRegistry.getInstance(); - FileType defaultFileType = defaultFileTypeName == null ? null : fileTypeRegistry.findFileTypeByName(defaultFileTypeName); + FileType defaultFileType = defaultfileType == null ? null : fileTypeRegistry.findFileTypeByName(defaultfileType); reader.beginArray(); while (reader.hasNext()) { String title = null; @@ -133,7 +160,7 @@ final class DiffHttpService extends RestService { if (name.equals("title")) { title = reader.nextString(); } - else if (name.equals("fileTypeName")) { + else if (name.equals("fileType")) { fileType = reader.nextString(); } else if (name.equals("content")) { @@ -145,7 +172,7 @@ final class DiffHttpService extends RestService { } reader.endObject(); - if (content == null) {; + if (content == null) { return "content is not specified"; } diff --git a/platform/platform-impl/src/org/jetbrains/ide/RestService.java b/platform/platform-impl/src/org/jetbrains/ide/RestService.java index 7c21f5d90900..5f4f29a6c085 100644 --- a/platform/platform-impl/src/org/jetbrains/ide/RestService.java +++ b/platform/platform-impl/src/org/jetbrains/ide/RestService.java @@ -24,6 +24,7 @@ import com.intellij.openapi.vfs.CharsetToolkit; import com.intellij.openapi.wm.IdeFocusManager; import com.intellij.openapi.wm.IdeFrame; import com.intellij.util.ExceptionUtil; +import com.intellij.util.containers.ContainerUtil; import io.netty.buffer.ByteBufInputStream; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.http.*; @@ -33,7 +34,12 @@ import org.jetbrains.io.Responses; import java.io.IOException; import java.io.InputStreamReader; +import java.util.List; +/** + * Document your service using apiDoc. To extract big example from source code, consider to use *.coffee file near your source file. + * (or Python/Ruby, but coffee recommended because it's plugin is lightweight). See {@link AboutHttpService} for example. + */ public abstract class RestService extends HttpRequestHandler { protected static final Logger LOG = Logger.getInstance(RestService.class); @@ -55,18 +61,21 @@ public abstract class RestService extends HttpRequestHandler { return true; } else { - char c = uri.charAt(minLength + 1); + char c = uri.charAt(minLength); return c == '/' || c == '?'; } } return false; } - protected abstract boolean isMethodSupported(@NotNull HttpMethod method); - @NotNull + /** + * Use human-readable name or UUID if it is an internal service. + */ protected abstract String getServiceName(); + protected abstract boolean isMethodSupported(@NotNull HttpMethod method); + @Override public final boolean process(@NotNull QueryStringDecoder urlDecoder, @NotNull FullHttpRequest request, @NotNull ChannelHandlerContext context) throws IOException { try { @@ -115,4 +124,15 @@ public abstract class RestService extends HttpRequestHandler { } return project; } + + protected static boolean getBooleanParameter(@NotNull String name, @NotNull QueryStringDecoder urlDecoder) { + List values = urlDecoder.parameters().get(name); + if (ContainerUtil.isEmpty(values)) { + return false; + } + + String value = values.get(values.size() - 1); + // if just name specified, so, true + return value.isEmpty() || Boolean.parseBoolean(value); + } } diff --git a/platform/platform-resources/src/META-INF/PlatformExtensions.xml b/platform/platform-resources/src/META-INF/PlatformExtensions.xml index efc3fa5c07bb..c1adabb5dda0 100644 --- a/platform/platform-resources/src/META-INF/PlatformExtensions.xml +++ b/platform/platform-resources/src/META-INF/PlatformExtensions.xml @@ -341,6 +341,7 @@ + From 83285b82a6156e4c9ab8749adf3d9c537347e910 Mon Sep 17 00:00:00 2001 From: Aleksey Pivovarov Date: Mon, 2 Feb 2015 14:55:21 +0300 Subject: [PATCH 2/3] IDEA-135836: fix editor layout scrollbar could be positioned on the left (EditorEx.VERTICAL_SCROLLBAR_LEFT) --- .../src/com/intellij/ui/components/JBScrollPane.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/platform/platform-api/src/com/intellij/ui/components/JBScrollPane.java b/platform/platform-api/src/com/intellij/ui/components/JBScrollPane.java index 1a926e66f333..4cf61591605c 100644 --- a/platform/platform-api/src/com/intellij/ui/components/JBScrollPane.java +++ b/platform/platform-api/src/com/intellij/ui/components/JBScrollPane.java @@ -219,14 +219,16 @@ public class JBScrollPane extends JScrollPane { if (hsb != null) container.setComponentZOrder(hsb, 0); if (extendViewportUnderVScrollbar) { + int x2 = Math.max(vsb.getX() + vsb.getWidth(), viewportBounds.x + viewportBounds.width); viewportBounds.x = Math.min(viewportBounds.x, vsb.getX()); - viewportBounds.width = vsb.getX() + vsb.getWidth() - viewportBounds.x; + viewportBounds.width = x2 - viewportBounds.x; } if (extendViewportUnderHScrollbar) { + int y2 = Math.max(hsb.getY() + hsb.getHeight(), viewportBounds.y + viewportBounds.height); viewportBounds.y = Math.min(viewportBounds.y, hsb.getY()); - viewportBounds.height = hsb.getY() + hsb.getHeight() - viewportBounds.y; + viewportBounds.height = y2 - viewportBounds.y; } - + if (extendViewportUnderVScrollbar) { if (hsb != null) { Rectangle scrollbarBounds = hsb.getBounds(); From bbba239d837f67ab5990bf4d9eade2e578089fe5 Mon Sep 17 00:00:00 2001 From: Dennis Ushakov Date: Mon, 2 Feb 2015 15:39:54 +0300 Subject: [PATCH 3/3] don't allocate new array for non-multi root files --- platform/core-impl/src/com/intellij/psi/stubs/StubTree.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/platform/core-impl/src/com/intellij/psi/stubs/StubTree.java b/platform/core-impl/src/com/intellij/psi/stubs/StubTree.java index 16488845acff..fa5bc5b3de91 100644 --- a/platform/core-impl/src/com/intellij/psi/stubs/StubTree.java +++ b/platform/core-impl/src/com/intellij/psi/stubs/StubTree.java @@ -62,7 +62,10 @@ public class StubTree extends ObjectStubTree> { @NotNull @Override public List> getPlainListFromAllRoots() { - return ContainerUtil.concat(getRoot().getStubRoots(), new Function>>() { + final PsiFileStub[] roots = getRoot().getStubRoots(); + if (roots.length == 1) return super.getPlainListFromAllRoots(); + + return ContainerUtil.concat(roots, new Function>>() { @Override public Collection> fun(PsiFileStub stub) { final ObjectStubTree existingTree = stub.getUserData(STUB_TO_TREE_REFERENCE);