From 11d38a75ed91e2cdd76d18fb0bba4b27124bea3f Mon Sep 17 00:00:00 2001 From: "microsoft-github-operations[bot]" <55726097+microsoft-github-operations[bot]@users.noreply.github.com> Date: Wed, 4 Dec 2024 02:31:04 +0000 Subject: [PATCH 001/690] Initial commit --- .gitignore | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..524f0963 --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* +replay_pid* From 2bcf3f5e23eff122d6b34d8825bcf5a86dc2a948 Mon Sep 17 00:00:00 2001 From: Sheng Chen Date: Wed, 4 Dec 2024 15:31:11 +0800 Subject: [PATCH 002/690] build - Initial commit to setup Repo. (#4) * Setup Checkstyle for the project. * Add module Core, which stores code that is not related with ui. * Add module UI, which stores code that is UI realted, for example SWT related code. --- .gitignore | 6 + .project | 17 + .settings/copilot4eclipse-cleanup-profile.xml | 14 + .settings/dict | 2 + .../github4eclipse-formatter-profile.xml | 164 +++++++ .settings/org.eclipse.m2e.core.prefs | 4 + checkstyle.xml | 440 ++++++++++++++++++ .../.checkstyle | 7 + com.microsoft.copilot.eclipse.core/.classpath | 7 + com.microsoft.copilot.eclipse.core/.project | 40 ++ .../.settings/org.eclipse.jdt.core.prefs | 9 + .../.settings/org.eclipse.m2e.core.prefs | 4 + .../META-INF/MANIFEST.MF | 10 + .../build.properties | 6 + com.microsoft.copilot.eclipse.core/plugin.xml | 4 + com.microsoft.copilot.eclipse.core/pom.xml | 28 ++ .../eclipse/core/CopilotCorePlugin.java | 21 + com.microsoft.copilot.eclipse.ui/.checkstyle | 7 + com.microsoft.copilot.eclipse.ui/.classpath | 7 + com.microsoft.copilot.eclipse.ui/.project | 40 ++ .../.settings/org.eclipse.jdt.core.prefs | 9 + .../.settings/org.eclipse.m2e.core.prefs | 4 + .../META-INF/MANIFEST.MF | 11 + .../build.properties | 5 + com.microsoft.copilot.eclipse.ui/plugin.xml | 4 + com.microsoft.copilot.eclipse.ui/pom.xml | 28 ++ .../copilot/eclipse/ui/CopilotUiPlugin.java | 21 + pom.xml | 97 ++++ target-platform.target | 42 ++ 29 files changed, 1058 insertions(+) create mode 100644 .project create mode 100644 .settings/copilot4eclipse-cleanup-profile.xml create mode 100644 .settings/dict create mode 100644 .settings/github4eclipse-formatter-profile.xml create mode 100644 .settings/org.eclipse.m2e.core.prefs create mode 100644 checkstyle.xml create mode 100644 com.microsoft.copilot.eclipse.core/.checkstyle create mode 100644 com.microsoft.copilot.eclipse.core/.classpath create mode 100644 com.microsoft.copilot.eclipse.core/.project create mode 100644 com.microsoft.copilot.eclipse.core/.settings/org.eclipse.jdt.core.prefs create mode 100644 com.microsoft.copilot.eclipse.core/.settings/org.eclipse.m2e.core.prefs create mode 100644 com.microsoft.copilot.eclipse.core/META-INF/MANIFEST.MF create mode 100644 com.microsoft.copilot.eclipse.core/build.properties create mode 100644 com.microsoft.copilot.eclipse.core/plugin.xml create mode 100644 com.microsoft.copilot.eclipse.core/pom.xml create mode 100644 com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/CopilotCorePlugin.java create mode 100644 com.microsoft.copilot.eclipse.ui/.checkstyle create mode 100644 com.microsoft.copilot.eclipse.ui/.classpath create mode 100644 com.microsoft.copilot.eclipse.ui/.project create mode 100644 com.microsoft.copilot.eclipse.ui/.settings/org.eclipse.jdt.core.prefs create mode 100644 com.microsoft.copilot.eclipse.ui/.settings/org.eclipse.m2e.core.prefs create mode 100644 com.microsoft.copilot.eclipse.ui/META-INF/MANIFEST.MF create mode 100644 com.microsoft.copilot.eclipse.ui/build.properties create mode 100644 com.microsoft.copilot.eclipse.ui/plugin.xml create mode 100644 com.microsoft.copilot.eclipse.ui/pom.xml create mode 100644 com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/CopilotUiPlugin.java create mode 100644 pom.xml create mode 100644 target-platform.target diff --git a/.gitignore b/.gitignore index 524f0963..6cb923f3 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,9 @@ # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml hs_err_pid* replay_pid* + +# Mac files +.DS_Store + +## Maven +**/target/ diff --git a/.project b/.project new file mode 100644 index 00000000..71c768cb --- /dev/null +++ b/.project @@ -0,0 +1,17 @@ + + + github-copilot-for-eclipse + + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.m2e.core.maven2Nature + + diff --git a/.settings/copilot4eclipse-cleanup-profile.xml b/.settings/copilot4eclipse-cleanup-profile.xml new file mode 100644 index 00000000..230109d1 --- /dev/null +++ b/.settings/copilot4eclipse-cleanup-profile.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + diff --git a/.settings/dict b/.settings/dict new file mode 100644 index 00000000..dc6a5502 --- /dev/null +++ b/.settings/dict @@ -0,0 +1,2 @@ +copilot +plugin diff --git a/.settings/github4eclipse-formatter-profile.xml b/.settings/github4eclipse-formatter-profile.xml new file mode 100644 index 00000000..b3e3d0e4 --- /dev/null +++ b/.settings/github4eclipse-formatter-profile.xml @@ -0,0 +1,164 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.settings/org.eclipse.m2e.core.prefs b/.settings/org.eclipse.m2e.core.prefs new file mode 100644 index 00000000..f897a7f1 --- /dev/null +++ b/.settings/org.eclipse.m2e.core.prefs @@ -0,0 +1,4 @@ +activeProfiles= +eclipse.preferences.version=1 +resolveWorkspaceProjects=true +version=1 diff --git a/checkstyle.xml b/checkstyle.xml new file mode 100644 index 00000000..24d9ebf3 --- /dev/null +++ b/checkstyle.xml @@ -0,0 +1,440 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/com.microsoft.copilot.eclipse.core/.checkstyle b/com.microsoft.copilot.eclipse.core/.checkstyle new file mode 100644 index 00000000..357a8477 --- /dev/null +++ b/com.microsoft.copilot.eclipse.core/.checkstyle @@ -0,0 +1,7 @@ + + + + + + + diff --git a/com.microsoft.copilot.eclipse.core/.classpath b/com.microsoft.copilot.eclipse.core/.classpath new file mode 100644 index 00000000..8d861214 --- /dev/null +++ b/com.microsoft.copilot.eclipse.core/.classpath @@ -0,0 +1,7 @@ + + + + + + + diff --git a/com.microsoft.copilot.eclipse.core/.project b/com.microsoft.copilot.eclipse.core/.project new file mode 100644 index 00000000..957d06d4 --- /dev/null +++ b/com.microsoft.copilot.eclipse.core/.project @@ -0,0 +1,40 @@ + + + com.microsoft.copilot.eclipse.core + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.pde.ManifestBuilder + + + + + org.eclipse.pde.SchemaBuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + net.sf.eclipsecs.core.CheckstyleBuilder + + + + + + org.eclipse.m2e.core.maven2Nature + org.eclipse.pde.PluginNature + org.eclipse.jdt.core.javanature + net.sf.eclipsecs.core.CheckstyleNature + + diff --git a/com.microsoft.copilot.eclipse.core/.settings/org.eclipse.jdt.core.prefs b/com.microsoft.copilot.eclipse.core/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 00000000..62ef3488 --- /dev/null +++ b/com.microsoft.copilot.eclipse.core/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,9 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.targetPlatform=17 +org.eclipse.jdt.core.compiler.compliance=17 +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=warning +org.eclipse.jdt.core.compiler.release=enabled +org.eclipse.jdt.core.compiler.source=17 diff --git a/com.microsoft.copilot.eclipse.core/.settings/org.eclipse.m2e.core.prefs b/com.microsoft.copilot.eclipse.core/.settings/org.eclipse.m2e.core.prefs new file mode 100644 index 00000000..f897a7f1 --- /dev/null +++ b/com.microsoft.copilot.eclipse.core/.settings/org.eclipse.m2e.core.prefs @@ -0,0 +1,4 @@ +activeProfiles= +eclipse.preferences.version=1 +resolveWorkspaceProjects=true +version=1 diff --git a/com.microsoft.copilot.eclipse.core/META-INF/MANIFEST.MF b/com.microsoft.copilot.eclipse.core/META-INF/MANIFEST.MF new file mode 100644 index 00000000..e92232fc --- /dev/null +++ b/com.microsoft.copilot.eclipse.core/META-INF/MANIFEST.MF @@ -0,0 +1,10 @@ +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Name: com.microsoft.copilot.eclipse.core +Bundle-SymbolicName: com.microsoft.copilot.eclipse.core;singleton:=true +Bundle-Version: 0.1.0.qualifier +Bundle-Activator: com.microsoft.copilot.eclipse.core.CopilotCorePlugin +Bundle-RequiredExecutionEnvironment: JavaSE-17 +Automatic-Module-Name: com.microsoft.copilot.eclipse.core +Bundle-ActivationPolicy: lazy +Import-Package: org.osgi.framework;version="[1.10.0,2.0.0)" diff --git a/com.microsoft.copilot.eclipse.core/build.properties b/com.microsoft.copilot.eclipse.core/build.properties new file mode 100644 index 00000000..4c15b450 --- /dev/null +++ b/com.microsoft.copilot.eclipse.core/build.properties @@ -0,0 +1,6 @@ +source.. = src +output.. = target/classes +bin.includes = META-INF/,\ + .,\ + plugin.xml + diff --git a/com.microsoft.copilot.eclipse.core/plugin.xml b/com.microsoft.copilot.eclipse.core/plugin.xml new file mode 100644 index 00000000..f422d55d --- /dev/null +++ b/com.microsoft.copilot.eclipse.core/plugin.xml @@ -0,0 +1,4 @@ + + + + diff --git a/com.microsoft.copilot.eclipse.core/pom.xml b/com.microsoft.copilot.eclipse.core/pom.xml new file mode 100644 index 00000000..f6e55b27 --- /dev/null +++ b/com.microsoft.copilot.eclipse.core/pom.xml @@ -0,0 +1,28 @@ + + 4.0.0 + + com.microsoft.copilot.eclipse + github-copilot-for-eclipse + 0.1.0-SNAPSHOT + + com.microsoft.copilot.eclipse.core + eclipse-plugin + ${base.name} :: Core + + + + org.eclipse.tycho + target-platform-configuration + ${tycho-version} + + + org.eclipse.tycho + tycho-maven-plugin + ${tycho-version} + true + + + + \ No newline at end of file diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/CopilotCorePlugin.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/CopilotCorePlugin.java new file mode 100644 index 00000000..c6ef7af5 --- /dev/null +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/CopilotCorePlugin.java @@ -0,0 +1,21 @@ +package com.microsoft.copilot.eclipse.core; + +import org.osgi.framework.BundleActivator; +import org.osgi.framework.BundleContext; + +/** + * Activator class for the Copilot core plugin. + */ +public class CopilotCorePlugin implements BundleActivator { + + @Override + public void start(BundleContext context) throws Exception { + + } + + @Override + public void stop(BundleContext context) throws Exception { + + } + +} diff --git a/com.microsoft.copilot.eclipse.ui/.checkstyle b/com.microsoft.copilot.eclipse.ui/.checkstyle new file mode 100644 index 00000000..357a8477 --- /dev/null +++ b/com.microsoft.copilot.eclipse.ui/.checkstyle @@ -0,0 +1,7 @@ + + + + + + + diff --git a/com.microsoft.copilot.eclipse.ui/.classpath b/com.microsoft.copilot.eclipse.ui/.classpath new file mode 100644 index 00000000..8d861214 --- /dev/null +++ b/com.microsoft.copilot.eclipse.ui/.classpath @@ -0,0 +1,7 @@ + + + + + + + diff --git a/com.microsoft.copilot.eclipse.ui/.project b/com.microsoft.copilot.eclipse.ui/.project new file mode 100644 index 00000000..81edfba7 --- /dev/null +++ b/com.microsoft.copilot.eclipse.ui/.project @@ -0,0 +1,40 @@ + + + com.microsoft.copilot.eclipse.ui + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.pde.ManifestBuilder + + + + + org.eclipse.pde.SchemaBuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + net.sf.eclipsecs.core.CheckstyleBuilder + + + + + + org.eclipse.m2e.core.maven2Nature + org.eclipse.pde.PluginNature + org.eclipse.jdt.core.javanature + net.sf.eclipsecs.core.CheckstyleNature + + diff --git a/com.microsoft.copilot.eclipse.ui/.settings/org.eclipse.jdt.core.prefs b/com.microsoft.copilot.eclipse.ui/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 00000000..62ef3488 --- /dev/null +++ b/com.microsoft.copilot.eclipse.ui/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,9 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.targetPlatform=17 +org.eclipse.jdt.core.compiler.compliance=17 +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=warning +org.eclipse.jdt.core.compiler.release=enabled +org.eclipse.jdt.core.compiler.source=17 diff --git a/com.microsoft.copilot.eclipse.ui/.settings/org.eclipse.m2e.core.prefs b/com.microsoft.copilot.eclipse.ui/.settings/org.eclipse.m2e.core.prefs new file mode 100644 index 00000000..f897a7f1 --- /dev/null +++ b/com.microsoft.copilot.eclipse.ui/.settings/org.eclipse.m2e.core.prefs @@ -0,0 +1,4 @@ +activeProfiles= +eclipse.preferences.version=1 +resolveWorkspaceProjects=true +version=1 diff --git a/com.microsoft.copilot.eclipse.ui/META-INF/MANIFEST.MF b/com.microsoft.copilot.eclipse.ui/META-INF/MANIFEST.MF new file mode 100644 index 00000000..b46c543d --- /dev/null +++ b/com.microsoft.copilot.eclipse.ui/META-INF/MANIFEST.MF @@ -0,0 +1,11 @@ +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Name: com.microsoft.copilot.eclipse.ui +Bundle-SymbolicName: com.microsoft.copilot.eclipse.ui;singleton:=true +Bundle-Version: 0.1.0.qualifier +Bundle-Activator: com.microsoft.copilot.eclipse.ui.CopilotUiPlugin +Bundle-RequiredExecutionEnvironment: JavaSE-17 +Automatic-Module-Name: com.microsoft.copilot.eclipse.ui +Bundle-ActivationPolicy: lazy +Import-Package: org.osgi.framework;version="[1.10.0,2.0.0)" +Require-Bundle: com.microsoft.copilot.eclipse.core;bundle-version="0.1.0" diff --git a/com.microsoft.copilot.eclipse.ui/build.properties b/com.microsoft.copilot.eclipse.ui/build.properties new file mode 100644 index 00000000..206f1966 --- /dev/null +++ b/com.microsoft.copilot.eclipse.ui/build.properties @@ -0,0 +1,5 @@ +source.. = src +output.. = target/classes +bin.includes = META-INF/,\ + .,\ + plugin.xml diff --git a/com.microsoft.copilot.eclipse.ui/plugin.xml b/com.microsoft.copilot.eclipse.ui/plugin.xml new file mode 100644 index 00000000..f422d55d --- /dev/null +++ b/com.microsoft.copilot.eclipse.ui/plugin.xml @@ -0,0 +1,4 @@ + + + + diff --git a/com.microsoft.copilot.eclipse.ui/pom.xml b/com.microsoft.copilot.eclipse.ui/pom.xml new file mode 100644 index 00000000..8da60f2b --- /dev/null +++ b/com.microsoft.copilot.eclipse.ui/pom.xml @@ -0,0 +1,28 @@ + + 4.0.0 + + com.microsoft.copilot.eclipse + github-copilot-for-eclipse + 0.1.0-SNAPSHOT + + com.microsoft.copilot.eclipse.ui + eclipse-plugin + ${base.name} :: UI + + + + org.eclipse.tycho + target-platform-configuration + ${tycho-version} + + + org.eclipse.tycho + tycho-maven-plugin + ${tycho-version} + true + + + + \ No newline at end of file diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/CopilotUiPlugin.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/CopilotUiPlugin.java new file mode 100644 index 00000000..9a0a59cd --- /dev/null +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/CopilotUiPlugin.java @@ -0,0 +1,21 @@ +package com.microsoft.copilot.eclipse.ui; + +import org.osgi.framework.BundleActivator; +import org.osgi.framework.BundleContext; + +/** + * Activator class for the Copilot UI plugin. + */ +public class CopilotUiPlugin implements BundleActivator { + + @Override + public void start(BundleContext context) throws Exception { + + } + + @Override + public void stop(BundleContext context) throws Exception { + + } + +} diff --git a/pom.xml b/pom.xml new file mode 100644 index 00000000..6a6090b3 --- /dev/null +++ b/pom.xml @@ -0,0 +1,97 @@ + + + 4.0.0 + com.microsoft.copilot.eclipse + github-copilot-for-eclipse + 0.1.0-SNAPSHOT + pom + ${base.name} + + + GitHub Copilot for Eclipse + 4.0.10 + 3.6.0 + + + + com.microsoft.copilot.eclipse.core + com.microsoft.copilot.eclipse.ui + + + + + + org.apache.maven.plugins + maven-checkstyle-plugin + ${checkstyle-version} + + + com.puppycrawl.tools + checkstyle + 10.20.1 + + + + checkstyle.xml + UTF-8 + true + true + + + + verify + + check + + + + + + org.eclipse.tycho + tycho-maven-plugin + ${tycho-version} + true + + + org.eclipse.tycho + target-platform-configuration + ${tycho-version} + + + ../target-platform.target + + + + macosx + cocoa + x86_64 + + + macosx + cocoa + aarch64 + + + linux + gtk + x86_64 + + + linux + gtk + aarch64 + + + win32 + win32 + x86_64 + + + + + + + \ No newline at end of file diff --git a/target-platform.target b/target-platform.target new file mode 100644 index 00000000..e29da0bc --- /dev/null +++ b/target-platform.target @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + org.apache.commons + commons-lang3 + 3.17.0 + jar + + + + + + + io.reactivex.rxjava3 + rxjava + 3.1.10 + + + + + + \ No newline at end of file From 31e10ba11718dd822236e26b3fc47d31e72b152b Mon Sep 17 00:00:00 2001 From: Sheng Chen Date: Wed, 4 Dec 2024 16:03:20 +0800 Subject: [PATCH 003/690] build - Initialize CI pipeline (#5) --- .azure-pipelines/ci.yml | 48 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 .azure-pipelines/ci.yml diff --git a/.azure-pipelines/ci.yml b/.azure-pipelines/ci.yml new file mode 100644 index 00000000..2d2a1db9 --- /dev/null +++ b/.azure-pipelines/ci.yml @@ -0,0 +1,48 @@ +name: $(Date:yyyyMMdd).$(Rev:r) +variables: + - name: Codeql.Enabled + value: true +resources: + repositories: + - repository: self + type: git + ref: refs/heads/main + - repository: 1esPipelines + type: git + name: 1ESPipelineTemplates/1ESPipelineTemplates + ref: refs/tags/release +trigger: + branches: + include: + - main +extends: + template: v1/1ES.Unofficial.PipelineTemplate.yml@1esPipelines + parameters: + pool: + os: linux + name: 1ES_JavaTooling_Pool + image: 1ES_JavaTooling_Ubuntu-2004 + sdl: + sourceAnalysisPool: + name: 1ES_JavaTooling_Pool + image: 1ES_JavaTooling_Windows_2022 + os: windows + stages: + - stage: Build + jobs: + - job: Build + displayName: GitHub-Copilot-Eclipse-CI + steps: + - checkout: self + fetchTags: false + - task: JavaToolInstaller@0 + displayName: Use Java 17 + inputs: + versionSpec: "17" + jdkArchitectureOption: x64 + jdkSourceOption: PreInstalled + - task: Maven@4 + displayName: 'Run Maven Clean and Verify' + inputs: + goals: 'clean verify' + publishJUnitResults: false \ No newline at end of file From 912fc46edcf6801056732cb7471405f2f9860e50 Mon Sep 17 00:00:00 2001 From: Sheng Chen Date: Thu, 5 Dec 2024 10:51:19 +0800 Subject: [PATCH 004/690] build - Add GitHub Actions for CI (#7) --- .azure-pipelines/ci.yml | 48 ----- .github/workflows/ci.yml | 28 +++ .mvn/wrapper/maven-wrapper.properties | 19 ++ mvnw | 259 ++++++++++++++++++++++++++ mvnw.cmd | 149 +++++++++++++++ 5 files changed, 455 insertions(+), 48 deletions(-) delete mode 100644 .azure-pipelines/ci.yml create mode 100644 .github/workflows/ci.yml create mode 100644 .mvn/wrapper/maven-wrapper.properties create mode 100755 mvnw create mode 100644 mvnw.cmd diff --git a/.azure-pipelines/ci.yml b/.azure-pipelines/ci.yml deleted file mode 100644 index 2d2a1db9..00000000 --- a/.azure-pipelines/ci.yml +++ /dev/null @@ -1,48 +0,0 @@ -name: $(Date:yyyyMMdd).$(Rev:r) -variables: - - name: Codeql.Enabled - value: true -resources: - repositories: - - repository: self - type: git - ref: refs/heads/main - - repository: 1esPipelines - type: git - name: 1ESPipelineTemplates/1ESPipelineTemplates - ref: refs/tags/release -trigger: - branches: - include: - - main -extends: - template: v1/1ES.Unofficial.PipelineTemplate.yml@1esPipelines - parameters: - pool: - os: linux - name: 1ES_JavaTooling_Pool - image: 1ES_JavaTooling_Ubuntu-2004 - sdl: - sourceAnalysisPool: - name: 1ES_JavaTooling_Pool - image: 1ES_JavaTooling_Windows_2022 - os: windows - stages: - - stage: Build - jobs: - - job: Build - displayName: GitHub-Copilot-Eclipse-CI - steps: - - checkout: self - fetchTags: false - - task: JavaToolInstaller@0 - displayName: Use Java 17 - inputs: - versionSpec: "17" - jdkArchitectureOption: x64 - jdkSourceOption: PreInstalled - - task: Maven@4 - displayName: 'Run Maven Clean and Verify' - inputs: - goals: 'clean verify' - publishJUnitResults: false \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..94386370 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,28 @@ +name: CI + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + cache: maven + - name: Build with Maven + run: ./mvnw clean verify + + # Uploads the full dependency graph to GitHub to improve the quality of Dependabot alerts this repository can receive + - name: Update dependency graph + uses: advanced-security/maven-dependency-submission-action@571e99aab1055c2e71a1e2309b9691de18d6b7d6 + continue-on-error: true diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 00000000..d58dfb70 --- /dev/null +++ b/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,19 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. +wrapperVersion=3.3.2 +distributionType=only-script +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip diff --git a/mvnw b/mvnw new file mode 100755 index 00000000..19529ddf --- /dev/null +++ b/mvnw @@ -0,0 +1,259 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.3.2 +# +# Optional ENV vars +# ----------------- +# JAVA_HOME - location of a JDK home dir, required when download maven via java source +# MVNW_REPOURL - repo url base for downloading maven distribution +# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output +# ---------------------------------------------------------------------------- + +set -euf +[ "${MVNW_VERBOSE-}" != debug ] || set -x + +# OS specific support. +native_path() { printf %s\\n "$1"; } +case "$(uname)" in +CYGWIN* | MINGW*) + [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" + native_path() { cygpath --path --windows "$1"; } + ;; +esac + +# set JAVACMD and JAVACCMD +set_java_home() { + # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched + if [ -n "${JAVA_HOME-}" ]; then + if [ -x "$JAVA_HOME/jre/sh/java" ]; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACCMD="$JAVA_HOME/jre/sh/javac" + else + JAVACMD="$JAVA_HOME/bin/java" + JAVACCMD="$JAVA_HOME/bin/javac" + + if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then + echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 + echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 + return 1 + fi + fi + else + JAVACMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v java + )" || : + JAVACCMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v javac + )" || : + + if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then + echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 + return 1 + fi + fi +} + +# hash string like Java String::hashCode +hash_string() { + str="${1:-}" h=0 + while [ -n "$str" ]; do + char="${str%"${str#?}"}" + h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) + str="${str#?}" + done + printf %x\\n $h +} + +verbose() { :; } +[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } + +die() { + printf %s\\n "$1" >&2 + exit 1 +} + +trim() { + # MWRAPPER-139: + # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. + # Needed for removing poorly interpreted newline sequences when running in more + # exotic environments such as mingw bash on Windows. + printf "%s" "${1}" | tr -d '[:space:]' +} + +# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties +while IFS="=" read -r key value; do + case "${key-}" in + distributionUrl) distributionUrl=$(trim "${value-}") ;; + distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; + esac +done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties" +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties" + +case "${distributionUrl##*/}" in +maven-mvnd-*bin.*) + MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ + case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in + *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; + :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; + :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; + :Linux*x86_64*) distributionPlatform=linux-amd64 ;; + *) + echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 + distributionPlatform=linux-amd64 + ;; + esac + distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" + ;; +maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; +*) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; +esac + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" +distributionUrlName="${distributionUrl##*/}" +distributionUrlNameMain="${distributionUrlName%.*}" +distributionUrlNameMain="${distributionUrlNameMain%-bin}" +MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" +MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" + +exec_maven() { + unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : + exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" +} + +if [ -d "$MAVEN_HOME" ]; then + verbose "found existing MAVEN_HOME at $MAVEN_HOME" + exec_maven "$@" +fi + +case "${distributionUrl-}" in +*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; +*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; +esac + +# prepare tmp dir +if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then + clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } + trap clean HUP INT TERM EXIT +else + die "cannot create temp dir" +fi + +mkdir -p -- "${MAVEN_HOME%/*}" + +# Download and Install Apache Maven +verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +verbose "Downloading from: $distributionUrl" +verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +# select .zip or .tar.gz +if ! command -v unzip >/dev/null; then + distributionUrl="${distributionUrl%.zip}.tar.gz" + distributionUrlName="${distributionUrl##*/}" +fi + +# verbose opt +__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' +[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v + +# normalize http auth +case "${MVNW_PASSWORD:+has-password}" in +'') MVNW_USERNAME='' MVNW_PASSWORD='' ;; +has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; +esac + +if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then + verbose "Found wget ... using wget" + wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" +elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then + verbose "Found curl ... using curl" + curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" +elif set_java_home; then + verbose "Falling back to use Java to download" + javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" + targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" + cat >"$javaSource" <<-END + public class Downloader extends java.net.Authenticator + { + protected java.net.PasswordAuthentication getPasswordAuthentication() + { + return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); + } + public static void main( String[] args ) throws Exception + { + setDefault( new Downloader() ); + java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); + } + } + END + # For Cygwin/MinGW, switch paths to Windows format before running javac and java + verbose " - Compiling Downloader.java ..." + "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" + verbose " - Running Downloader.java ..." + "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" +fi + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +if [ -n "${distributionSha256Sum-}" ]; then + distributionSha256Result=false + if [ "$MVN_CMD" = mvnd.sh ]; then + echo "Checksum validation is not supported for maven-mvnd." >&2 + echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + elif command -v sha256sum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then + distributionSha256Result=true + fi + elif command -v shasum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then + distributionSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 + echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + fi + if [ $distributionSha256Result = false ]; then + echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 + echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 + exit 1 + fi +fi + +# unzip and move +if command -v unzip >/dev/null; then + unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" +else + tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" +fi +printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url" +mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" + +clean || : +exec_maven "$@" diff --git a/mvnw.cmd b/mvnw.cmd new file mode 100644 index 00000000..b150b91e --- /dev/null +++ b/mvnw.cmd @@ -0,0 +1,149 @@ +<# : batch portion +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.3.2 +@REM +@REM Optional ENV vars +@REM MVNW_REPOURL - repo url base for downloading maven distribution +@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output +@REM ---------------------------------------------------------------------------- + +@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) +@SET __MVNW_CMD__= +@SET __MVNW_ERROR__= +@SET __MVNW_PSMODULEP_SAVE=%PSModulePath% +@SET PSModulePath= +@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( + IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) +) +@SET PSModulePath=%__MVNW_PSMODULEP_SAVE% +@SET __MVNW_PSMODULEP_SAVE= +@SET __MVNW_ARG0_NAME__= +@SET MVNW_USERNAME= +@SET MVNW_PASSWORD= +@IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*) +@echo Cannot start maven from wrapper >&2 && exit /b 1 +@GOTO :EOF +: end batch / begin powershell #> + +$ErrorActionPreference = "Stop" +if ($env:MVNW_VERBOSE -eq "true") { + $VerbosePreference = "Continue" +} + +# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties +$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl +if (!$distributionUrl) { + Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" +} + +switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { + "maven-mvnd-*" { + $USE_MVND = $true + $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" + $MVN_CMD = "mvnd.cmd" + break + } + default { + $USE_MVND = $false + $MVN_CMD = $script -replace '^mvnw','mvn' + break + } +} + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +if ($env:MVNW_REPOURL) { + $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" } + $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')" +} +$distributionUrlName = $distributionUrl -replace '^.*/','' +$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' +$MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain" +if ($env:MAVEN_USER_HOME) { + $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain" +} +$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' +$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" + +if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { + Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" + Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" + exit $? +} + +if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { + Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" +} + +# prepare tmp dir +$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile +$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" +$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null +trap { + if ($TMP_DOWNLOAD_DIR.Exists) { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } + } +} + +New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null + +# Download and Install Apache Maven +Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +Write-Verbose "Downloading from: $distributionUrl" +Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +$webclient = New-Object System.Net.WebClient +if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { + $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) +} +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum +if ($distributionSha256Sum) { + if ($USE_MVND) { + Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." + } + Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash + if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { + Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." + } +} + +# unzip and move +Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null +try { + Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null +} catch { + if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { + Write-Error "fail to move MAVEN_HOME" + } +} finally { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } +} + +Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" From 7343af17069ab033c4ba2811bdb4070e37cf7b22 Mon Sep 17 00:00:00 2001 From: "microsoft-github-policy-service[bot]" <77245923+microsoft-github-policy-service[bot]@users.noreply.github.com> Date: Thu, 5 Dec 2024 10:52:28 +0800 Subject: [PATCH 005/690] eng - Microsoft mandatory file (#2) Co-authored-by: microsoft-github-policy-service[bot] <77245923+microsoft-github-policy-service[bot]@users.noreply.github.com> --- SECURITY.md | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 SECURITY.md diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..b3c89efc --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,41 @@ + + +## Security + +Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet) and [Xamarin](https://github.com/xamarin). + +If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/security.md/definition), please report it to us as described below. + +## Reporting Security Issues + +**Please do not report security vulnerabilities through public GitHub issues.** + +Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/security.md/msrc/create-report). + +If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/security.md/msrc/pgp). + +You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc). + +Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: + + * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) + * Full paths of source file(s) related to the manifestation of the issue + * The location of the affected source code (tag/branch/commit or direct URL) + * Any special configuration required to reproduce the issue + * Step-by-step instructions to reproduce the issue + * Proof-of-concept or exploit code (if possible) + * Impact of the issue, including how an attacker might exploit the issue + +This information will help us triage your report more quickly. + +If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/security.md/msrc/bounty) page for more details about our active programs. + +## Preferred Languages + +We prefer all communications to be in English. + +## Policy + +Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/security.md/cvd). + + From 5d25866875492420d71f860f3076276a76ce0b08 Mon Sep 17 00:00:00 2001 From: Sheng Chen Date: Thu, 5 Dec 2024 10:57:19 +0800 Subject: [PATCH 006/690] eng - Apply format and cleanup profile to Eclipse preference (#8) --- ... => copilot4eclipse-formatter-profile.xml} | 0 .../.settings/org.eclipse.jdt.core.prefs | 400 ++++++++++++++++++ .../.settings/org.eclipse.jdt.ui.prefs | 147 +++++++ .../.settings/org.eclipse.jdt.core.prefs | 400 ++++++++++++++++++ .../.settings/org.eclipse.jdt.ui.prefs | 147 +++++++ 5 files changed, 1094 insertions(+) rename .settings/{github4eclipse-formatter-profile.xml => copilot4eclipse-formatter-profile.xml} (100%) create mode 100644 com.microsoft.copilot.eclipse.core/.settings/org.eclipse.jdt.ui.prefs create mode 100644 com.microsoft.copilot.eclipse.ui/.settings/org.eclipse.jdt.ui.prefs diff --git a/.settings/github4eclipse-formatter-profile.xml b/.settings/copilot4eclipse-formatter-profile.xml similarity index 100% rename from .settings/github4eclipse-formatter-profile.xml rename to .settings/copilot4eclipse-formatter-profile.xml diff --git a/com.microsoft.copilot.eclipse.core/.settings/org.eclipse.jdt.core.prefs b/com.microsoft.copilot.eclipse.core/.settings/org.eclipse.jdt.core.prefs index 62ef3488..c42a96bf 100644 --- a/com.microsoft.copilot.eclipse.core/.settings/org.eclipse.jdt.core.prefs +++ b/com.microsoft.copilot.eclipse.core/.settings/org.eclipse.jdt.core.prefs @@ -7,3 +7,403 @@ org.eclipse.jdt.core.compiler.problem.enumIdentifier=error org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=warning org.eclipse.jdt.core.compiler.release=enabled org.eclipse.jdt.core.compiler.source=17 +org.eclipse.jdt.core.formatter.align_arrows_in_switch_on_columns=false +org.eclipse.jdt.core.formatter.align_assignment_statements_on_columns=false +org.eclipse.jdt.core.formatter.align_fields_grouping_blank_lines=2147483647 +org.eclipse.jdt.core.formatter.align_selector_in_method_invocation_on_expression_first_line=false +org.eclipse.jdt.core.formatter.align_type_members_on_columns=false +org.eclipse.jdt.core.formatter.align_variable_declarations_on_columns=false +org.eclipse.jdt.core.formatter.align_with_spaces=false +org.eclipse.jdt.core.formatter.alignment_for_additive_operator=16 +org.eclipse.jdt.core.formatter.alignment_for_annotations_on_enum_constant=0 +org.eclipse.jdt.core.formatter.alignment_for_annotations_on_field=0 +org.eclipse.jdt.core.formatter.alignment_for_annotations_on_local_variable=0 +org.eclipse.jdt.core.formatter.alignment_for_annotations_on_method=0 +org.eclipse.jdt.core.formatter.alignment_for_annotations_on_package=0 +org.eclipse.jdt.core.formatter.alignment_for_annotations_on_parameter=0 +org.eclipse.jdt.core.formatter.alignment_for_annotations_on_type=0 +org.eclipse.jdt.core.formatter.alignment_for_arguments_in_allocation_expression=16 +org.eclipse.jdt.core.formatter.alignment_for_arguments_in_annotation=0 +org.eclipse.jdt.core.formatter.alignment_for_arguments_in_enum_constant=16 +org.eclipse.jdt.core.formatter.alignment_for_arguments_in_explicit_constructor_call=16 +org.eclipse.jdt.core.formatter.alignment_for_arguments_in_method_invocation=16 +org.eclipse.jdt.core.formatter.alignment_for_arguments_in_qualified_allocation_expression=16 +org.eclipse.jdt.core.formatter.alignment_for_assertion_message=0 +org.eclipse.jdt.core.formatter.alignment_for_assignment=0 +org.eclipse.jdt.core.formatter.alignment_for_bitwise_operator=16 +org.eclipse.jdt.core.formatter.alignment_for_compact_if=16 +org.eclipse.jdt.core.formatter.alignment_for_compact_loops=16 +org.eclipse.jdt.core.formatter.alignment_for_conditional_expression=80 +org.eclipse.jdt.core.formatter.alignment_for_conditional_expression_chain=0 +org.eclipse.jdt.core.formatter.alignment_for_enum_constants=16 +org.eclipse.jdt.core.formatter.alignment_for_expressions_in_array_initializer=16 +org.eclipse.jdt.core.formatter.alignment_for_expressions_in_for_loop_header=0 +org.eclipse.jdt.core.formatter.alignment_for_expressions_in_switch_case_with_arrow=0 +org.eclipse.jdt.core.formatter.alignment_for_expressions_in_switch_case_with_colon=0 +org.eclipse.jdt.core.formatter.alignment_for_logical_operator=16 +org.eclipse.jdt.core.formatter.alignment_for_method_declaration=0 +org.eclipse.jdt.core.formatter.alignment_for_module_statements=16 +org.eclipse.jdt.core.formatter.alignment_for_multiple_fields=16 +org.eclipse.jdt.core.formatter.alignment_for_multiplicative_operator=16 +org.eclipse.jdt.core.formatter.alignment_for_parameterized_type_references=0 +org.eclipse.jdt.core.formatter.alignment_for_parameters_in_constructor_declaration=16 +org.eclipse.jdt.core.formatter.alignment_for_parameters_in_method_declaration=16 +org.eclipse.jdt.core.formatter.alignment_for_permitted_types_in_type_declaration=16 +org.eclipse.jdt.core.formatter.alignment_for_record_components=16 +org.eclipse.jdt.core.formatter.alignment_for_relational_operator=0 +org.eclipse.jdt.core.formatter.alignment_for_resources_in_try=80 +org.eclipse.jdt.core.formatter.alignment_for_selector_in_method_invocation=16 +org.eclipse.jdt.core.formatter.alignment_for_shift_operator=0 +org.eclipse.jdt.core.formatter.alignment_for_string_concatenation=16 +org.eclipse.jdt.core.formatter.alignment_for_superclass_in_type_declaration=16 +org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_enum_declaration=16 +org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_record_declaration=16 +org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_type_declaration=16 +org.eclipse.jdt.core.formatter.alignment_for_switch_case_with_arrow=0 +org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_constructor_declaration=16 +org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_method_declaration=16 +org.eclipse.jdt.core.formatter.alignment_for_type_annotations=0 +org.eclipse.jdt.core.formatter.alignment_for_type_arguments=0 +org.eclipse.jdt.core.formatter.alignment_for_type_parameters=0 +org.eclipse.jdt.core.formatter.alignment_for_union_type_in_multicatch=16 +org.eclipse.jdt.core.formatter.blank_lines_after_imports=1 +org.eclipse.jdt.core.formatter.blank_lines_after_last_class_body_declaration=0 +org.eclipse.jdt.core.formatter.blank_lines_after_package=1 +org.eclipse.jdt.core.formatter.blank_lines_before_abstract_method=1 +org.eclipse.jdt.core.formatter.blank_lines_before_field=0 +org.eclipse.jdt.core.formatter.blank_lines_before_first_class_body_declaration=0 +org.eclipse.jdt.core.formatter.blank_lines_before_imports=1 +org.eclipse.jdt.core.formatter.blank_lines_before_member_type=1 +org.eclipse.jdt.core.formatter.blank_lines_before_method=1 +org.eclipse.jdt.core.formatter.blank_lines_before_new_chunk=1 +org.eclipse.jdt.core.formatter.blank_lines_before_package=0 +org.eclipse.jdt.core.formatter.blank_lines_between_import_groups=1 +org.eclipse.jdt.core.formatter.blank_lines_between_statement_group_in_switch=0 +org.eclipse.jdt.core.formatter.blank_lines_between_type_declarations=1 +org.eclipse.jdt.core.formatter.brace_position_for_annotation_type_declaration=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_anonymous_type_declaration=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_array_initializer=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_block=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_block_in_case=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_block_in_case_after_arrow=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_constructor_declaration=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_enum_constant=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_enum_declaration=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_lambda_body=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_method_declaration=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_record_constructor=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_record_declaration=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_switch=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_type_declaration=end_of_line +org.eclipse.jdt.core.formatter.comment.align_tags_descriptions_grouped=false +org.eclipse.jdt.core.formatter.comment.align_tags_names_descriptions=false +org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_block_comment=false +org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_javadoc_comment=false +org.eclipse.jdt.core.formatter.comment.count_line_length_from_starting_position=false +org.eclipse.jdt.core.formatter.comment.format_block_comments=true +org.eclipse.jdt.core.formatter.comment.format_header=false +org.eclipse.jdt.core.formatter.comment.format_html=true +org.eclipse.jdt.core.formatter.comment.format_javadoc_comments=true +org.eclipse.jdt.core.formatter.comment.format_line_comments=true +org.eclipse.jdt.core.formatter.comment.format_source_code=true +org.eclipse.jdt.core.formatter.comment.indent_parameter_description=false +org.eclipse.jdt.core.formatter.comment.indent_root_tags=false +org.eclipse.jdt.core.formatter.comment.indent_tag_description=false +org.eclipse.jdt.core.formatter.comment.insert_new_line_before_root_tags=insert +org.eclipse.jdt.core.formatter.comment.insert_new_line_between_different_tags=do not insert +org.eclipse.jdt.core.formatter.comment.insert_new_line_for_parameter=do not insert +org.eclipse.jdt.core.formatter.comment.javadoc_do_not_separate_block_tags=false +org.eclipse.jdt.core.formatter.comment.line_length=120 +org.eclipse.jdt.core.formatter.comment.new_lines_at_block_boundaries=true +org.eclipse.jdt.core.formatter.comment.new_lines_at_javadoc_boundaries=true +org.eclipse.jdt.core.formatter.comment.preserve_white_space_between_code_and_line_comments=false +org.eclipse.jdt.core.formatter.compact_else_if=true +org.eclipse.jdt.core.formatter.continuation_indentation=2 +org.eclipse.jdt.core.formatter.continuation_indentation_for_array_initializer=2 +org.eclipse.jdt.core.formatter.disabling_tag=@formatter\:off +org.eclipse.jdt.core.formatter.enabling_tag=@formatter\:on +org.eclipse.jdt.core.formatter.format_guardian_clause_on_one_line=false +org.eclipse.jdt.core.formatter.format_line_comment_starting_on_first_column=false +org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_annotation_declaration_header=true +org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_constant_header=true +org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_declaration_header=true +org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_record_header=true +org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_type_header=true +org.eclipse.jdt.core.formatter.indent_breaks_compare_to_cases=true +org.eclipse.jdt.core.formatter.indent_empty_lines=false +org.eclipse.jdt.core.formatter.indent_statements_compare_to_block=true +org.eclipse.jdt.core.formatter.indent_statements_compare_to_body=true +org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_cases=true +org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_switch=false +org.eclipse.jdt.core.formatter.indentation.size=2 +org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_enum_constant=insert +org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_field=insert +org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_local_variable=insert +org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_method=insert +org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_package=insert +org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_parameter=do not insert +org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_type=insert +org.eclipse.jdt.core.formatter.insert_new_line_after_label=do not insert +org.eclipse.jdt.core.formatter.insert_new_line_after_opening_brace_in_array_initializer=do not insert +org.eclipse.jdt.core.formatter.insert_new_line_after_type_annotation=do not insert +org.eclipse.jdt.core.formatter.insert_new_line_at_end_of_file_if_missing=do not insert +org.eclipse.jdt.core.formatter.insert_new_line_before_catch_in_try_statement=do not insert +org.eclipse.jdt.core.formatter.insert_new_line_before_closing_brace_in_array_initializer=do not insert +org.eclipse.jdt.core.formatter.insert_new_line_before_else_in_if_statement=do not insert +org.eclipse.jdt.core.formatter.insert_new_line_before_finally_in_try_statement=insert +org.eclipse.jdt.core.formatter.insert_new_line_before_while_in_do_statement=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_additive_operator=insert +org.eclipse.jdt.core.formatter.insert_space_after_and_in_type_parameter=insert +org.eclipse.jdt.core.formatter.insert_space_after_arrow_in_switch_case=insert +org.eclipse.jdt.core.formatter.insert_space_after_arrow_in_switch_default=insert +org.eclipse.jdt.core.formatter.insert_space_after_assignment_operator=insert +org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation_type_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_bitwise_operator=insert +org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_arguments=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_parameters=insert +org.eclipse.jdt.core.formatter.insert_space_after_closing_brace_in_block=insert +org.eclipse.jdt.core.formatter.insert_space_after_closing_paren_in_cast=insert +org.eclipse.jdt.core.formatter.insert_space_after_colon_in_assert=insert +org.eclipse.jdt.core.formatter.insert_space_after_colon_in_case=insert +org.eclipse.jdt.core.formatter.insert_space_after_colon_in_conditional=insert +org.eclipse.jdt.core.formatter.insert_space_after_colon_in_for=insert +org.eclipse.jdt.core.formatter.insert_space_after_colon_in_labeled_statement=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_allocation_expression=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_annotation=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_array_initializer=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_parameters=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_throws=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_constant_arguments=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_declarations=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_explicitconstructorcall_arguments=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_increments=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_inits=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_parameters=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_throws=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_invocation_arguments=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_field_declarations=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_local_declarations=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_parameterized_type_reference=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_permitted_types=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_record_components=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_superinterfaces=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_switch_case_expressions=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_arguments=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_parameters=insert +org.eclipse.jdt.core.formatter.insert_space_after_ellipsis=insert +org.eclipse.jdt.core.formatter.insert_space_after_lambda_arrow=insert +org.eclipse.jdt.core.formatter.insert_space_after_logical_operator=insert +org.eclipse.jdt.core.formatter.insert_space_after_multiplicative_operator=insert +org.eclipse.jdt.core.formatter.insert_space_after_not_operator=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_parameterized_type_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_arguments=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_parameters=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_brace_in_array_initializer=insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_allocation_expression=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_annotation=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_cast=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_catch=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_constructor_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_enum_constant=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_for=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_if=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_invocation=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_parenthesized_expression=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_record_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_switch=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_synchronized=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_try=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_while=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_postfix_operator=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_prefix_operator=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_question_in_conditional=insert +org.eclipse.jdt.core.formatter.insert_space_after_question_in_wildcard=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_relational_operator=insert +org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_for=insert +org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_try_resources=insert +org.eclipse.jdt.core.formatter.insert_space_after_shift_operator=insert +org.eclipse.jdt.core.formatter.insert_space_after_string_concatenation=insert +org.eclipse.jdt.core.formatter.insert_space_after_unary_operator=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_additive_operator=insert +org.eclipse.jdt.core.formatter.insert_space_before_and_in_type_parameter=insert +org.eclipse.jdt.core.formatter.insert_space_before_arrow_in_switch_case=insert +org.eclipse.jdt.core.formatter.insert_space_before_arrow_in_switch_default=insert +org.eclipse.jdt.core.formatter.insert_space_before_assignment_operator=insert +org.eclipse.jdt.core.formatter.insert_space_before_at_in_annotation_type_declaration=insert +org.eclipse.jdt.core.formatter.insert_space_before_bitwise_operator=insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_parameterized_type_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_arguments=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_parameters=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_brace_in_array_initializer=insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_allocation_expression=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_annotation=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_cast=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_catch=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_constructor_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_enum_constant=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_for=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_if=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_invocation=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_parenthesized_expression=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_record_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_switch=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_synchronized=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_try=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_while=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_colon_in_assert=insert +org.eclipse.jdt.core.formatter.insert_space_before_colon_in_case=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_colon_in_conditional=insert +org.eclipse.jdt.core.formatter.insert_space_before_colon_in_default=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_colon_in_for=insert +org.eclipse.jdt.core.formatter.insert_space_before_colon_in_labeled_statement=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_allocation_expression=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_annotation=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_array_initializer=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_parameters=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_throws=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_constant_arguments=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_declarations=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_explicitconstructorcall_arguments=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_increments=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_inits=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_parameters=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_throws=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_invocation_arguments=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_field_declarations=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_local_declarations=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_parameterized_type_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_permitted_types=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_record_components=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_superinterfaces=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_switch_case_expressions=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_arguments=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_parameters=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_ellipsis=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_lambda_arrow=insert +org.eclipse.jdt.core.formatter.insert_space_before_logical_operator=insert +org.eclipse.jdt.core.formatter.insert_space_before_multiplicative_operator=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_parameterized_type_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_arguments=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_parameters=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_annotation_type_declaration=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_anonymous_type_declaration=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_array_initializer=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_block=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_constructor_declaration=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_constant=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_declaration=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_method_declaration=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_record_constructor=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_record_declaration=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_switch=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_type_declaration=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_allocation_expression=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_type_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation_type_member_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_catch=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_constructor_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_enum_constant=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_for=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_if=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_invocation=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_parenthesized_expression=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_record_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_switch=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_synchronized=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_try=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_while=insert +org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_return=insert +org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_throw=insert +org.eclipse.jdt.core.formatter.insert_space_before_postfix_operator=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_prefix_operator=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_question_in_conditional=insert +org.eclipse.jdt.core.formatter.insert_space_before_question_in_wildcard=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_relational_operator=insert +org.eclipse.jdt.core.formatter.insert_space_before_semicolon=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_for=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_try_resources=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_shift_operator=insert +org.eclipse.jdt.core.formatter.insert_space_before_string_concatenation=insert +org.eclipse.jdt.core.formatter.insert_space_before_unary_operator=do not insert +org.eclipse.jdt.core.formatter.insert_space_between_brackets_in_array_type_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_between_empty_braces_in_array_initializer=do not insert +org.eclipse.jdt.core.formatter.insert_space_between_empty_brackets_in_array_allocation_expression=do not insert +org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_annotation_type_member_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_constructor_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_enum_constant=do not insert +org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_invocation=do not insert +org.eclipse.jdt.core.formatter.join_line_comments=false +org.eclipse.jdt.core.formatter.join_lines_in_comments=true +org.eclipse.jdt.core.formatter.join_wrapped_lines=true +org.eclipse.jdt.core.formatter.keep_annotation_declaration_on_one_line=one_line_never +org.eclipse.jdt.core.formatter.keep_anonymous_type_declaration_on_one_line=one_line_never +org.eclipse.jdt.core.formatter.keep_code_block_on_one_line=one_line_never +org.eclipse.jdt.core.formatter.keep_else_statement_on_same_line=false +org.eclipse.jdt.core.formatter.keep_empty_array_initializer_on_one_line=false +org.eclipse.jdt.core.formatter.keep_enum_constant_declaration_on_one_line=one_line_never +org.eclipse.jdt.core.formatter.keep_enum_declaration_on_one_line=one_line_never +org.eclipse.jdt.core.formatter.keep_if_then_body_block_on_one_line=one_line_never +org.eclipse.jdt.core.formatter.keep_imple_if_on_one_line=false +org.eclipse.jdt.core.formatter.keep_lambda_body_block_on_one_line=one_line_never +org.eclipse.jdt.core.formatter.keep_loop_body_block_on_one_line=one_line_never +org.eclipse.jdt.core.formatter.keep_method_body_on_one_line=one_line_never +org.eclipse.jdt.core.formatter.keep_record_constructor_on_one_line=one_line_never +org.eclipse.jdt.core.formatter.keep_record_declaration_on_one_line=one_line_never +org.eclipse.jdt.core.formatter.keep_simple_do_while_body_on_same_line=false +org.eclipse.jdt.core.formatter.keep_simple_for_body_on_same_line=false +org.eclipse.jdt.core.formatter.keep_simple_getter_setter_on_one_line=false +org.eclipse.jdt.core.formatter.keep_simple_while_body_on_same_line=false +org.eclipse.jdt.core.formatter.keep_switch_body_block_on_one_line=one_line_never +org.eclipse.jdt.core.formatter.keep_switch_case_with_arrow_on_one_line=one_line_never +org.eclipse.jdt.core.formatter.keep_then_statement_on_same_line=false +org.eclipse.jdt.core.formatter.keep_type_declaration_on_one_line=one_line_never +org.eclipse.jdt.core.formatter.lineSplit=120 +org.eclipse.jdt.core.formatter.never_indent_block_comments_on_first_column=false +org.eclipse.jdt.core.formatter.never_indent_line_comments_on_first_column=false +org.eclipse.jdt.core.formatter.number_of_blank_lines_after_code_block=0 +org.eclipse.jdt.core.formatter.number_of_blank_lines_at_beginning_of_code_block=0 +org.eclipse.jdt.core.formatter.number_of_blank_lines_at_beginning_of_method_body=0 +org.eclipse.jdt.core.formatter.number_of_blank_lines_at_end_of_code_block=0 +org.eclipse.jdt.core.formatter.number_of_blank_lines_at_end_of_method_body=0 +org.eclipse.jdt.core.formatter.number_of_blank_lines_before_code_block=0 +org.eclipse.jdt.core.formatter.number_of_empty_lines_to_preserve=1 +org.eclipse.jdt.core.formatter.parentheses_positions_in_annotation=common_lines +org.eclipse.jdt.core.formatter.parentheses_positions_in_catch_clause=common_lines +org.eclipse.jdt.core.formatter.parentheses_positions_in_enum_constant_declaration=common_lines +org.eclipse.jdt.core.formatter.parentheses_positions_in_for_statment=common_lines +org.eclipse.jdt.core.formatter.parentheses_positions_in_if_while_statement=common_lines +org.eclipse.jdt.core.formatter.parentheses_positions_in_lambda_declaration=common_lines +org.eclipse.jdt.core.formatter.parentheses_positions_in_method_delcaration=common_lines +org.eclipse.jdt.core.formatter.parentheses_positions_in_method_invocation=common_lines +org.eclipse.jdt.core.formatter.parentheses_positions_in_record_declaration=common_lines +org.eclipse.jdt.core.formatter.parentheses_positions_in_switch_statement=common_lines +org.eclipse.jdt.core.formatter.parentheses_positions_in_try_clause=common_lines +org.eclipse.jdt.core.formatter.put_empty_statement_on_new_line=true +org.eclipse.jdt.core.formatter.tabulation.char=space +org.eclipse.jdt.core.formatter.tabulation.size=2 +org.eclipse.jdt.core.formatter.text_block_indentation=0 +org.eclipse.jdt.core.formatter.use_on_off_tags=true +org.eclipse.jdt.core.formatter.use_tabs_only_for_leading_indentations=false +org.eclipse.jdt.core.formatter.wrap_before_additive_operator=true +org.eclipse.jdt.core.formatter.wrap_before_assertion_message_operator=true +org.eclipse.jdt.core.formatter.wrap_before_assignment_operator=false +org.eclipse.jdt.core.formatter.wrap_before_bitwise_operator=true +org.eclipse.jdt.core.formatter.wrap_before_conditional_operator=true +org.eclipse.jdt.core.formatter.wrap_before_logical_operator=true +org.eclipse.jdt.core.formatter.wrap_before_multiplicative_operator=true +org.eclipse.jdt.core.formatter.wrap_before_or_operator_multicatch=true +org.eclipse.jdt.core.formatter.wrap_before_relational_operator=true +org.eclipse.jdt.core.formatter.wrap_before_shift_operator=true +org.eclipse.jdt.core.formatter.wrap_before_string_concatenation=true +org.eclipse.jdt.core.formatter.wrap_before_switch_case_arrow_operator=false +org.eclipse.jdt.core.formatter.wrap_outer_expressions_when_nested=true +org.eclipse.jdt.core.javaFormatter=org.eclipse.jdt.core.defaultJavaFormatter diff --git a/com.microsoft.copilot.eclipse.core/.settings/org.eclipse.jdt.ui.prefs b/com.microsoft.copilot.eclipse.core/.settings/org.eclipse.jdt.ui.prefs new file mode 100644 index 00000000..7f7518f0 --- /dev/null +++ b/com.microsoft.copilot.eclipse.core/.settings/org.eclipse.jdt.ui.prefs @@ -0,0 +1,147 @@ +cleanup.add_all=false +cleanup.add_default_serial_version_id=true +cleanup.add_generated_serial_version_id=false +cleanup.add_missing_annotations=true +cleanup.add_missing_deprecated_annotations=true +cleanup.add_missing_methods=false +cleanup.add_missing_nls_tags=false +cleanup.add_missing_override_annotations=true +cleanup.add_missing_override_annotations_interface_methods=true +cleanup.add_serial_version_id=false +cleanup.also_simplify_lambda=true +cleanup.always_use_blocks=true +cleanup.always_use_parentheses_in_expressions=false +cleanup.always_use_this_for_non_static_field_access=false +cleanup.always_use_this_for_non_static_method_access=false +cleanup.array_with_curly=false +cleanup.arrays_fill=false +cleanup.bitwise_conditional_expression=false +cleanup.boolean_literal=false +cleanup.boolean_value_rather_than_comparison=true +cleanup.break_loop=false +cleanup.collection_cloning=false +cleanup.comparing_on_criteria=false +cleanup.comparison_statement=false +cleanup.controlflow_merge=false +cleanup.convert_functional_interfaces=false +cleanup.convert_to_enhanced_for_loop=true +cleanup.convert_to_enhanced_for_loop_if_loop_var_used=true +cleanup.convert_to_switch_expressions=false +cleanup.correct_indentation=false +cleanup.do_while_rather_than_while=true +cleanup.double_negation=false +cleanup.else_if=false +cleanup.embedded_if=false +cleanup.evaluate_nullable=false +cleanup.extract_increment=false +cleanup.format_source_code=true +cleanup.format_source_code_changes_only=false +cleanup.hash=false +cleanup.if_condition=false +cleanup.insert_inferred_type_arguments=false +cleanup.instanceof=false +cleanup.instanceof_keyword=false +cleanup.invert_equals=false +cleanup.join=false +cleanup.lazy_logical_operator=false +cleanup.make_local_variable_final=true +cleanup.make_parameters_final=false +cleanup.make_private_fields_final=true +cleanup.make_type_abstract_if_missing_method=false +cleanup.make_variable_declarations_final=false +cleanup.map_cloning=false +cleanup.merge_conditional_blocks=false +cleanup.multi_catch=false +cleanup.never_use_blocks=false +cleanup.never_use_parentheses_in_expressions=true +cleanup.no_string_creation=false +cleanup.no_super=false +cleanup.number_suffix=false +cleanup.objects_equals=false +cleanup.one_if_rather_than_duplicate_blocks_that_fall_through=true +cleanup.operand_factorization=false +cleanup.organize_imports=true +cleanup.overridden_assignment=false +cleanup.overridden_assignment_move_decl=true +cleanup.plain_replacement=false +cleanup.precompile_regex=false +cleanup.primitive_comparison=false +cleanup.primitive_parsing=false +cleanup.primitive_rather_than_wrapper=true +cleanup.primitive_serialization=false +cleanup.pull_out_if_from_if_else=false +cleanup.pull_up_assignment=false +cleanup.push_down_negation=false +cleanup.qualify_static_field_accesses_with_declaring_class=false +cleanup.qualify_static_member_accesses_through_instances_with_declaring_class=true +cleanup.qualify_static_member_accesses_through_subtypes_with_declaring_class=true +cleanup.qualify_static_member_accesses_with_declaring_class=true +cleanup.qualify_static_method_accesses_with_declaring_class=false +cleanup.reduce_indentation=false +cleanup.redundant_comparator=false +cleanup.redundant_falling_through_block_end=false +cleanup.remove_private_constructors=true +cleanup.remove_redundant_modifiers=false +cleanup.remove_redundant_semicolons=true +cleanup.remove_redundant_type_arguments=true +cleanup.remove_trailing_whitespaces=true +cleanup.remove_trailing_whitespaces_all=true +cleanup.remove_trailing_whitespaces_ignore_empty=false +cleanup.remove_unnecessary_array_creation=false +cleanup.remove_unnecessary_casts=true +cleanup.remove_unnecessary_nls_tags=true +cleanup.remove_unused_imports=true +cleanup.remove_unused_local_variables=false +cleanup.remove_unused_method_parameters=false +cleanup.remove_unused_private_fields=true +cleanup.remove_unused_private_members=false +cleanup.remove_unused_private_methods=true +cleanup.remove_unused_private_types=true +cleanup.replace_deprecated_calls=false +cleanup.return_expression=false +cleanup.simplify_lambda_expression_and_method_ref=false +cleanup.single_used_field=false +cleanup.sort_members=false +cleanup.sort_members_all=false +cleanup.standard_comparison=false +cleanup.static_inner_class=false +cleanup.strictly_equal_or_different=false +cleanup.stringbuffer_to_stringbuilder=false +cleanup.stringbuilder=false +cleanup.stringbuilder_for_local_vars=true +cleanup.stringconcat_stringbuffer_stringbuilder=false +cleanup.stringconcat_to_textblock=false +cleanup.substring=false +cleanup.switch=false +cleanup.system_property=false +cleanup.system_property_boolean=false +cleanup.system_property_file_encoding=false +cleanup.system_property_file_separator=false +cleanup.system_property_line_separator=false +cleanup.system_property_path_separator=false +cleanup.ternary_operator=false +cleanup.try_with_resource=false +cleanup.unlooped_while=false +cleanup.unreachable_block=false +cleanup.use_anonymous_class_creation=false +cleanup.use_autoboxing=false +cleanup.use_blocks=true +cleanup.use_blocks_only_for_return_and_throw=false +cleanup.use_directly_map_method=false +cleanup.use_lambda=true +cleanup.use_parentheses_in_expressions=false +cleanup.use_string_is_blank=false +cleanup.use_this_for_non_static_field_access=false +cleanup.use_this_for_non_static_field_access_only_if_necessary=true +cleanup.use_this_for_non_static_method_access=false +cleanup.use_this_for_non_static_method_access_only_if_necessary=true +cleanup.use_unboxing=false +cleanup.use_var=false +cleanup.useless_continue=false +cleanup.useless_return=false +cleanup.valueof_rather_than_instantiation=false +cleanup_profile=_CheckStyle-Generated github-copilot-for-eclipse +cleanup_settings_version=2 +eclipse.preferences.version=1 +formatter_profile=_CheckStyle-Generated github-copilot-for-eclipse +formatter_settings_version=23 diff --git a/com.microsoft.copilot.eclipse.ui/.settings/org.eclipse.jdt.core.prefs b/com.microsoft.copilot.eclipse.ui/.settings/org.eclipse.jdt.core.prefs index 62ef3488..c42a96bf 100644 --- a/com.microsoft.copilot.eclipse.ui/.settings/org.eclipse.jdt.core.prefs +++ b/com.microsoft.copilot.eclipse.ui/.settings/org.eclipse.jdt.core.prefs @@ -7,3 +7,403 @@ org.eclipse.jdt.core.compiler.problem.enumIdentifier=error org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=warning org.eclipse.jdt.core.compiler.release=enabled org.eclipse.jdt.core.compiler.source=17 +org.eclipse.jdt.core.formatter.align_arrows_in_switch_on_columns=false +org.eclipse.jdt.core.formatter.align_assignment_statements_on_columns=false +org.eclipse.jdt.core.formatter.align_fields_grouping_blank_lines=2147483647 +org.eclipse.jdt.core.formatter.align_selector_in_method_invocation_on_expression_first_line=false +org.eclipse.jdt.core.formatter.align_type_members_on_columns=false +org.eclipse.jdt.core.formatter.align_variable_declarations_on_columns=false +org.eclipse.jdt.core.formatter.align_with_spaces=false +org.eclipse.jdt.core.formatter.alignment_for_additive_operator=16 +org.eclipse.jdt.core.formatter.alignment_for_annotations_on_enum_constant=0 +org.eclipse.jdt.core.formatter.alignment_for_annotations_on_field=0 +org.eclipse.jdt.core.formatter.alignment_for_annotations_on_local_variable=0 +org.eclipse.jdt.core.formatter.alignment_for_annotations_on_method=0 +org.eclipse.jdt.core.formatter.alignment_for_annotations_on_package=0 +org.eclipse.jdt.core.formatter.alignment_for_annotations_on_parameter=0 +org.eclipse.jdt.core.formatter.alignment_for_annotations_on_type=0 +org.eclipse.jdt.core.formatter.alignment_for_arguments_in_allocation_expression=16 +org.eclipse.jdt.core.formatter.alignment_for_arguments_in_annotation=0 +org.eclipse.jdt.core.formatter.alignment_for_arguments_in_enum_constant=16 +org.eclipse.jdt.core.formatter.alignment_for_arguments_in_explicit_constructor_call=16 +org.eclipse.jdt.core.formatter.alignment_for_arguments_in_method_invocation=16 +org.eclipse.jdt.core.formatter.alignment_for_arguments_in_qualified_allocation_expression=16 +org.eclipse.jdt.core.formatter.alignment_for_assertion_message=0 +org.eclipse.jdt.core.formatter.alignment_for_assignment=0 +org.eclipse.jdt.core.formatter.alignment_for_bitwise_operator=16 +org.eclipse.jdt.core.formatter.alignment_for_compact_if=16 +org.eclipse.jdt.core.formatter.alignment_for_compact_loops=16 +org.eclipse.jdt.core.formatter.alignment_for_conditional_expression=80 +org.eclipse.jdt.core.formatter.alignment_for_conditional_expression_chain=0 +org.eclipse.jdt.core.formatter.alignment_for_enum_constants=16 +org.eclipse.jdt.core.formatter.alignment_for_expressions_in_array_initializer=16 +org.eclipse.jdt.core.formatter.alignment_for_expressions_in_for_loop_header=0 +org.eclipse.jdt.core.formatter.alignment_for_expressions_in_switch_case_with_arrow=0 +org.eclipse.jdt.core.formatter.alignment_for_expressions_in_switch_case_with_colon=0 +org.eclipse.jdt.core.formatter.alignment_for_logical_operator=16 +org.eclipse.jdt.core.formatter.alignment_for_method_declaration=0 +org.eclipse.jdt.core.formatter.alignment_for_module_statements=16 +org.eclipse.jdt.core.formatter.alignment_for_multiple_fields=16 +org.eclipse.jdt.core.formatter.alignment_for_multiplicative_operator=16 +org.eclipse.jdt.core.formatter.alignment_for_parameterized_type_references=0 +org.eclipse.jdt.core.formatter.alignment_for_parameters_in_constructor_declaration=16 +org.eclipse.jdt.core.formatter.alignment_for_parameters_in_method_declaration=16 +org.eclipse.jdt.core.formatter.alignment_for_permitted_types_in_type_declaration=16 +org.eclipse.jdt.core.formatter.alignment_for_record_components=16 +org.eclipse.jdt.core.formatter.alignment_for_relational_operator=0 +org.eclipse.jdt.core.formatter.alignment_for_resources_in_try=80 +org.eclipse.jdt.core.formatter.alignment_for_selector_in_method_invocation=16 +org.eclipse.jdt.core.formatter.alignment_for_shift_operator=0 +org.eclipse.jdt.core.formatter.alignment_for_string_concatenation=16 +org.eclipse.jdt.core.formatter.alignment_for_superclass_in_type_declaration=16 +org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_enum_declaration=16 +org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_record_declaration=16 +org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_type_declaration=16 +org.eclipse.jdt.core.formatter.alignment_for_switch_case_with_arrow=0 +org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_constructor_declaration=16 +org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_method_declaration=16 +org.eclipse.jdt.core.formatter.alignment_for_type_annotations=0 +org.eclipse.jdt.core.formatter.alignment_for_type_arguments=0 +org.eclipse.jdt.core.formatter.alignment_for_type_parameters=0 +org.eclipse.jdt.core.formatter.alignment_for_union_type_in_multicatch=16 +org.eclipse.jdt.core.formatter.blank_lines_after_imports=1 +org.eclipse.jdt.core.formatter.blank_lines_after_last_class_body_declaration=0 +org.eclipse.jdt.core.formatter.blank_lines_after_package=1 +org.eclipse.jdt.core.formatter.blank_lines_before_abstract_method=1 +org.eclipse.jdt.core.formatter.blank_lines_before_field=0 +org.eclipse.jdt.core.formatter.blank_lines_before_first_class_body_declaration=0 +org.eclipse.jdt.core.formatter.blank_lines_before_imports=1 +org.eclipse.jdt.core.formatter.blank_lines_before_member_type=1 +org.eclipse.jdt.core.formatter.blank_lines_before_method=1 +org.eclipse.jdt.core.formatter.blank_lines_before_new_chunk=1 +org.eclipse.jdt.core.formatter.blank_lines_before_package=0 +org.eclipse.jdt.core.formatter.blank_lines_between_import_groups=1 +org.eclipse.jdt.core.formatter.blank_lines_between_statement_group_in_switch=0 +org.eclipse.jdt.core.formatter.blank_lines_between_type_declarations=1 +org.eclipse.jdt.core.formatter.brace_position_for_annotation_type_declaration=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_anonymous_type_declaration=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_array_initializer=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_block=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_block_in_case=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_block_in_case_after_arrow=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_constructor_declaration=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_enum_constant=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_enum_declaration=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_lambda_body=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_method_declaration=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_record_constructor=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_record_declaration=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_switch=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_type_declaration=end_of_line +org.eclipse.jdt.core.formatter.comment.align_tags_descriptions_grouped=false +org.eclipse.jdt.core.formatter.comment.align_tags_names_descriptions=false +org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_block_comment=false +org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_javadoc_comment=false +org.eclipse.jdt.core.formatter.comment.count_line_length_from_starting_position=false +org.eclipse.jdt.core.formatter.comment.format_block_comments=true +org.eclipse.jdt.core.formatter.comment.format_header=false +org.eclipse.jdt.core.formatter.comment.format_html=true +org.eclipse.jdt.core.formatter.comment.format_javadoc_comments=true +org.eclipse.jdt.core.formatter.comment.format_line_comments=true +org.eclipse.jdt.core.formatter.comment.format_source_code=true +org.eclipse.jdt.core.formatter.comment.indent_parameter_description=false +org.eclipse.jdt.core.formatter.comment.indent_root_tags=false +org.eclipse.jdt.core.formatter.comment.indent_tag_description=false +org.eclipse.jdt.core.formatter.comment.insert_new_line_before_root_tags=insert +org.eclipse.jdt.core.formatter.comment.insert_new_line_between_different_tags=do not insert +org.eclipse.jdt.core.formatter.comment.insert_new_line_for_parameter=do not insert +org.eclipse.jdt.core.formatter.comment.javadoc_do_not_separate_block_tags=false +org.eclipse.jdt.core.formatter.comment.line_length=120 +org.eclipse.jdt.core.formatter.comment.new_lines_at_block_boundaries=true +org.eclipse.jdt.core.formatter.comment.new_lines_at_javadoc_boundaries=true +org.eclipse.jdt.core.formatter.comment.preserve_white_space_between_code_and_line_comments=false +org.eclipse.jdt.core.formatter.compact_else_if=true +org.eclipse.jdt.core.formatter.continuation_indentation=2 +org.eclipse.jdt.core.formatter.continuation_indentation_for_array_initializer=2 +org.eclipse.jdt.core.formatter.disabling_tag=@formatter\:off +org.eclipse.jdt.core.formatter.enabling_tag=@formatter\:on +org.eclipse.jdt.core.formatter.format_guardian_clause_on_one_line=false +org.eclipse.jdt.core.formatter.format_line_comment_starting_on_first_column=false +org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_annotation_declaration_header=true +org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_constant_header=true +org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_declaration_header=true +org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_record_header=true +org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_type_header=true +org.eclipse.jdt.core.formatter.indent_breaks_compare_to_cases=true +org.eclipse.jdt.core.formatter.indent_empty_lines=false +org.eclipse.jdt.core.formatter.indent_statements_compare_to_block=true +org.eclipse.jdt.core.formatter.indent_statements_compare_to_body=true +org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_cases=true +org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_switch=false +org.eclipse.jdt.core.formatter.indentation.size=2 +org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_enum_constant=insert +org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_field=insert +org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_local_variable=insert +org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_method=insert +org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_package=insert +org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_parameter=do not insert +org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_type=insert +org.eclipse.jdt.core.formatter.insert_new_line_after_label=do not insert +org.eclipse.jdt.core.formatter.insert_new_line_after_opening_brace_in_array_initializer=do not insert +org.eclipse.jdt.core.formatter.insert_new_line_after_type_annotation=do not insert +org.eclipse.jdt.core.formatter.insert_new_line_at_end_of_file_if_missing=do not insert +org.eclipse.jdt.core.formatter.insert_new_line_before_catch_in_try_statement=do not insert +org.eclipse.jdt.core.formatter.insert_new_line_before_closing_brace_in_array_initializer=do not insert +org.eclipse.jdt.core.formatter.insert_new_line_before_else_in_if_statement=do not insert +org.eclipse.jdt.core.formatter.insert_new_line_before_finally_in_try_statement=insert +org.eclipse.jdt.core.formatter.insert_new_line_before_while_in_do_statement=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_additive_operator=insert +org.eclipse.jdt.core.formatter.insert_space_after_and_in_type_parameter=insert +org.eclipse.jdt.core.formatter.insert_space_after_arrow_in_switch_case=insert +org.eclipse.jdt.core.formatter.insert_space_after_arrow_in_switch_default=insert +org.eclipse.jdt.core.formatter.insert_space_after_assignment_operator=insert +org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation_type_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_bitwise_operator=insert +org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_arguments=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_parameters=insert +org.eclipse.jdt.core.formatter.insert_space_after_closing_brace_in_block=insert +org.eclipse.jdt.core.formatter.insert_space_after_closing_paren_in_cast=insert +org.eclipse.jdt.core.formatter.insert_space_after_colon_in_assert=insert +org.eclipse.jdt.core.formatter.insert_space_after_colon_in_case=insert +org.eclipse.jdt.core.formatter.insert_space_after_colon_in_conditional=insert +org.eclipse.jdt.core.formatter.insert_space_after_colon_in_for=insert +org.eclipse.jdt.core.formatter.insert_space_after_colon_in_labeled_statement=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_allocation_expression=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_annotation=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_array_initializer=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_parameters=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_throws=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_constant_arguments=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_declarations=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_explicitconstructorcall_arguments=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_increments=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_inits=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_parameters=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_throws=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_invocation_arguments=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_field_declarations=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_local_declarations=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_parameterized_type_reference=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_permitted_types=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_record_components=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_superinterfaces=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_switch_case_expressions=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_arguments=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_parameters=insert +org.eclipse.jdt.core.formatter.insert_space_after_ellipsis=insert +org.eclipse.jdt.core.formatter.insert_space_after_lambda_arrow=insert +org.eclipse.jdt.core.formatter.insert_space_after_logical_operator=insert +org.eclipse.jdt.core.formatter.insert_space_after_multiplicative_operator=insert +org.eclipse.jdt.core.formatter.insert_space_after_not_operator=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_parameterized_type_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_arguments=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_parameters=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_brace_in_array_initializer=insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_allocation_expression=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_annotation=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_cast=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_catch=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_constructor_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_enum_constant=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_for=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_if=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_invocation=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_parenthesized_expression=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_record_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_switch=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_synchronized=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_try=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_while=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_postfix_operator=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_prefix_operator=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_question_in_conditional=insert +org.eclipse.jdt.core.formatter.insert_space_after_question_in_wildcard=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_relational_operator=insert +org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_for=insert +org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_try_resources=insert +org.eclipse.jdt.core.formatter.insert_space_after_shift_operator=insert +org.eclipse.jdt.core.formatter.insert_space_after_string_concatenation=insert +org.eclipse.jdt.core.formatter.insert_space_after_unary_operator=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_additive_operator=insert +org.eclipse.jdt.core.formatter.insert_space_before_and_in_type_parameter=insert +org.eclipse.jdt.core.formatter.insert_space_before_arrow_in_switch_case=insert +org.eclipse.jdt.core.formatter.insert_space_before_arrow_in_switch_default=insert +org.eclipse.jdt.core.formatter.insert_space_before_assignment_operator=insert +org.eclipse.jdt.core.formatter.insert_space_before_at_in_annotation_type_declaration=insert +org.eclipse.jdt.core.formatter.insert_space_before_bitwise_operator=insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_parameterized_type_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_arguments=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_parameters=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_brace_in_array_initializer=insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_allocation_expression=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_annotation=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_cast=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_catch=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_constructor_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_enum_constant=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_for=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_if=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_invocation=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_parenthesized_expression=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_record_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_switch=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_synchronized=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_try=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_while=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_colon_in_assert=insert +org.eclipse.jdt.core.formatter.insert_space_before_colon_in_case=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_colon_in_conditional=insert +org.eclipse.jdt.core.formatter.insert_space_before_colon_in_default=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_colon_in_for=insert +org.eclipse.jdt.core.formatter.insert_space_before_colon_in_labeled_statement=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_allocation_expression=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_annotation=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_array_initializer=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_parameters=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_throws=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_constant_arguments=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_declarations=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_explicitconstructorcall_arguments=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_increments=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_inits=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_parameters=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_throws=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_invocation_arguments=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_field_declarations=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_local_declarations=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_parameterized_type_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_permitted_types=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_record_components=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_superinterfaces=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_switch_case_expressions=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_arguments=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_parameters=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_ellipsis=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_lambda_arrow=insert +org.eclipse.jdt.core.formatter.insert_space_before_logical_operator=insert +org.eclipse.jdt.core.formatter.insert_space_before_multiplicative_operator=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_parameterized_type_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_arguments=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_parameters=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_annotation_type_declaration=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_anonymous_type_declaration=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_array_initializer=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_block=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_constructor_declaration=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_constant=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_declaration=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_method_declaration=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_record_constructor=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_record_declaration=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_switch=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_type_declaration=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_allocation_expression=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_type_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation_type_member_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_catch=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_constructor_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_enum_constant=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_for=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_if=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_invocation=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_parenthesized_expression=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_record_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_switch=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_synchronized=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_try=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_while=insert +org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_return=insert +org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_throw=insert +org.eclipse.jdt.core.formatter.insert_space_before_postfix_operator=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_prefix_operator=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_question_in_conditional=insert +org.eclipse.jdt.core.formatter.insert_space_before_question_in_wildcard=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_relational_operator=insert +org.eclipse.jdt.core.formatter.insert_space_before_semicolon=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_for=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_try_resources=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_shift_operator=insert +org.eclipse.jdt.core.formatter.insert_space_before_string_concatenation=insert +org.eclipse.jdt.core.formatter.insert_space_before_unary_operator=do not insert +org.eclipse.jdt.core.formatter.insert_space_between_brackets_in_array_type_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_between_empty_braces_in_array_initializer=do not insert +org.eclipse.jdt.core.formatter.insert_space_between_empty_brackets_in_array_allocation_expression=do not insert +org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_annotation_type_member_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_constructor_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_enum_constant=do not insert +org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_invocation=do not insert +org.eclipse.jdt.core.formatter.join_line_comments=false +org.eclipse.jdt.core.formatter.join_lines_in_comments=true +org.eclipse.jdt.core.formatter.join_wrapped_lines=true +org.eclipse.jdt.core.formatter.keep_annotation_declaration_on_one_line=one_line_never +org.eclipse.jdt.core.formatter.keep_anonymous_type_declaration_on_one_line=one_line_never +org.eclipse.jdt.core.formatter.keep_code_block_on_one_line=one_line_never +org.eclipse.jdt.core.formatter.keep_else_statement_on_same_line=false +org.eclipse.jdt.core.formatter.keep_empty_array_initializer_on_one_line=false +org.eclipse.jdt.core.formatter.keep_enum_constant_declaration_on_one_line=one_line_never +org.eclipse.jdt.core.formatter.keep_enum_declaration_on_one_line=one_line_never +org.eclipse.jdt.core.formatter.keep_if_then_body_block_on_one_line=one_line_never +org.eclipse.jdt.core.formatter.keep_imple_if_on_one_line=false +org.eclipse.jdt.core.formatter.keep_lambda_body_block_on_one_line=one_line_never +org.eclipse.jdt.core.formatter.keep_loop_body_block_on_one_line=one_line_never +org.eclipse.jdt.core.formatter.keep_method_body_on_one_line=one_line_never +org.eclipse.jdt.core.formatter.keep_record_constructor_on_one_line=one_line_never +org.eclipse.jdt.core.formatter.keep_record_declaration_on_one_line=one_line_never +org.eclipse.jdt.core.formatter.keep_simple_do_while_body_on_same_line=false +org.eclipse.jdt.core.formatter.keep_simple_for_body_on_same_line=false +org.eclipse.jdt.core.formatter.keep_simple_getter_setter_on_one_line=false +org.eclipse.jdt.core.formatter.keep_simple_while_body_on_same_line=false +org.eclipse.jdt.core.formatter.keep_switch_body_block_on_one_line=one_line_never +org.eclipse.jdt.core.formatter.keep_switch_case_with_arrow_on_one_line=one_line_never +org.eclipse.jdt.core.formatter.keep_then_statement_on_same_line=false +org.eclipse.jdt.core.formatter.keep_type_declaration_on_one_line=one_line_never +org.eclipse.jdt.core.formatter.lineSplit=120 +org.eclipse.jdt.core.formatter.never_indent_block_comments_on_first_column=false +org.eclipse.jdt.core.formatter.never_indent_line_comments_on_first_column=false +org.eclipse.jdt.core.formatter.number_of_blank_lines_after_code_block=0 +org.eclipse.jdt.core.formatter.number_of_blank_lines_at_beginning_of_code_block=0 +org.eclipse.jdt.core.formatter.number_of_blank_lines_at_beginning_of_method_body=0 +org.eclipse.jdt.core.formatter.number_of_blank_lines_at_end_of_code_block=0 +org.eclipse.jdt.core.formatter.number_of_blank_lines_at_end_of_method_body=0 +org.eclipse.jdt.core.formatter.number_of_blank_lines_before_code_block=0 +org.eclipse.jdt.core.formatter.number_of_empty_lines_to_preserve=1 +org.eclipse.jdt.core.formatter.parentheses_positions_in_annotation=common_lines +org.eclipse.jdt.core.formatter.parentheses_positions_in_catch_clause=common_lines +org.eclipse.jdt.core.formatter.parentheses_positions_in_enum_constant_declaration=common_lines +org.eclipse.jdt.core.formatter.parentheses_positions_in_for_statment=common_lines +org.eclipse.jdt.core.formatter.parentheses_positions_in_if_while_statement=common_lines +org.eclipse.jdt.core.formatter.parentheses_positions_in_lambda_declaration=common_lines +org.eclipse.jdt.core.formatter.parentheses_positions_in_method_delcaration=common_lines +org.eclipse.jdt.core.formatter.parentheses_positions_in_method_invocation=common_lines +org.eclipse.jdt.core.formatter.parentheses_positions_in_record_declaration=common_lines +org.eclipse.jdt.core.formatter.parentheses_positions_in_switch_statement=common_lines +org.eclipse.jdt.core.formatter.parentheses_positions_in_try_clause=common_lines +org.eclipse.jdt.core.formatter.put_empty_statement_on_new_line=true +org.eclipse.jdt.core.formatter.tabulation.char=space +org.eclipse.jdt.core.formatter.tabulation.size=2 +org.eclipse.jdt.core.formatter.text_block_indentation=0 +org.eclipse.jdt.core.formatter.use_on_off_tags=true +org.eclipse.jdt.core.formatter.use_tabs_only_for_leading_indentations=false +org.eclipse.jdt.core.formatter.wrap_before_additive_operator=true +org.eclipse.jdt.core.formatter.wrap_before_assertion_message_operator=true +org.eclipse.jdt.core.formatter.wrap_before_assignment_operator=false +org.eclipse.jdt.core.formatter.wrap_before_bitwise_operator=true +org.eclipse.jdt.core.formatter.wrap_before_conditional_operator=true +org.eclipse.jdt.core.formatter.wrap_before_logical_operator=true +org.eclipse.jdt.core.formatter.wrap_before_multiplicative_operator=true +org.eclipse.jdt.core.formatter.wrap_before_or_operator_multicatch=true +org.eclipse.jdt.core.formatter.wrap_before_relational_operator=true +org.eclipse.jdt.core.formatter.wrap_before_shift_operator=true +org.eclipse.jdt.core.formatter.wrap_before_string_concatenation=true +org.eclipse.jdt.core.formatter.wrap_before_switch_case_arrow_operator=false +org.eclipse.jdt.core.formatter.wrap_outer_expressions_when_nested=true +org.eclipse.jdt.core.javaFormatter=org.eclipse.jdt.core.defaultJavaFormatter diff --git a/com.microsoft.copilot.eclipse.ui/.settings/org.eclipse.jdt.ui.prefs b/com.microsoft.copilot.eclipse.ui/.settings/org.eclipse.jdt.ui.prefs new file mode 100644 index 00000000..7f7518f0 --- /dev/null +++ b/com.microsoft.copilot.eclipse.ui/.settings/org.eclipse.jdt.ui.prefs @@ -0,0 +1,147 @@ +cleanup.add_all=false +cleanup.add_default_serial_version_id=true +cleanup.add_generated_serial_version_id=false +cleanup.add_missing_annotations=true +cleanup.add_missing_deprecated_annotations=true +cleanup.add_missing_methods=false +cleanup.add_missing_nls_tags=false +cleanup.add_missing_override_annotations=true +cleanup.add_missing_override_annotations_interface_methods=true +cleanup.add_serial_version_id=false +cleanup.also_simplify_lambda=true +cleanup.always_use_blocks=true +cleanup.always_use_parentheses_in_expressions=false +cleanup.always_use_this_for_non_static_field_access=false +cleanup.always_use_this_for_non_static_method_access=false +cleanup.array_with_curly=false +cleanup.arrays_fill=false +cleanup.bitwise_conditional_expression=false +cleanup.boolean_literal=false +cleanup.boolean_value_rather_than_comparison=true +cleanup.break_loop=false +cleanup.collection_cloning=false +cleanup.comparing_on_criteria=false +cleanup.comparison_statement=false +cleanup.controlflow_merge=false +cleanup.convert_functional_interfaces=false +cleanup.convert_to_enhanced_for_loop=true +cleanup.convert_to_enhanced_for_loop_if_loop_var_used=true +cleanup.convert_to_switch_expressions=false +cleanup.correct_indentation=false +cleanup.do_while_rather_than_while=true +cleanup.double_negation=false +cleanup.else_if=false +cleanup.embedded_if=false +cleanup.evaluate_nullable=false +cleanup.extract_increment=false +cleanup.format_source_code=true +cleanup.format_source_code_changes_only=false +cleanup.hash=false +cleanup.if_condition=false +cleanup.insert_inferred_type_arguments=false +cleanup.instanceof=false +cleanup.instanceof_keyword=false +cleanup.invert_equals=false +cleanup.join=false +cleanup.lazy_logical_operator=false +cleanup.make_local_variable_final=true +cleanup.make_parameters_final=false +cleanup.make_private_fields_final=true +cleanup.make_type_abstract_if_missing_method=false +cleanup.make_variable_declarations_final=false +cleanup.map_cloning=false +cleanup.merge_conditional_blocks=false +cleanup.multi_catch=false +cleanup.never_use_blocks=false +cleanup.never_use_parentheses_in_expressions=true +cleanup.no_string_creation=false +cleanup.no_super=false +cleanup.number_suffix=false +cleanup.objects_equals=false +cleanup.one_if_rather_than_duplicate_blocks_that_fall_through=true +cleanup.operand_factorization=false +cleanup.organize_imports=true +cleanup.overridden_assignment=false +cleanup.overridden_assignment_move_decl=true +cleanup.plain_replacement=false +cleanup.precompile_regex=false +cleanup.primitive_comparison=false +cleanup.primitive_parsing=false +cleanup.primitive_rather_than_wrapper=true +cleanup.primitive_serialization=false +cleanup.pull_out_if_from_if_else=false +cleanup.pull_up_assignment=false +cleanup.push_down_negation=false +cleanup.qualify_static_field_accesses_with_declaring_class=false +cleanup.qualify_static_member_accesses_through_instances_with_declaring_class=true +cleanup.qualify_static_member_accesses_through_subtypes_with_declaring_class=true +cleanup.qualify_static_member_accesses_with_declaring_class=true +cleanup.qualify_static_method_accesses_with_declaring_class=false +cleanup.reduce_indentation=false +cleanup.redundant_comparator=false +cleanup.redundant_falling_through_block_end=false +cleanup.remove_private_constructors=true +cleanup.remove_redundant_modifiers=false +cleanup.remove_redundant_semicolons=true +cleanup.remove_redundant_type_arguments=true +cleanup.remove_trailing_whitespaces=true +cleanup.remove_trailing_whitespaces_all=true +cleanup.remove_trailing_whitespaces_ignore_empty=false +cleanup.remove_unnecessary_array_creation=false +cleanup.remove_unnecessary_casts=true +cleanup.remove_unnecessary_nls_tags=true +cleanup.remove_unused_imports=true +cleanup.remove_unused_local_variables=false +cleanup.remove_unused_method_parameters=false +cleanup.remove_unused_private_fields=true +cleanup.remove_unused_private_members=false +cleanup.remove_unused_private_methods=true +cleanup.remove_unused_private_types=true +cleanup.replace_deprecated_calls=false +cleanup.return_expression=false +cleanup.simplify_lambda_expression_and_method_ref=false +cleanup.single_used_field=false +cleanup.sort_members=false +cleanup.sort_members_all=false +cleanup.standard_comparison=false +cleanup.static_inner_class=false +cleanup.strictly_equal_or_different=false +cleanup.stringbuffer_to_stringbuilder=false +cleanup.stringbuilder=false +cleanup.stringbuilder_for_local_vars=true +cleanup.stringconcat_stringbuffer_stringbuilder=false +cleanup.stringconcat_to_textblock=false +cleanup.substring=false +cleanup.switch=false +cleanup.system_property=false +cleanup.system_property_boolean=false +cleanup.system_property_file_encoding=false +cleanup.system_property_file_separator=false +cleanup.system_property_line_separator=false +cleanup.system_property_path_separator=false +cleanup.ternary_operator=false +cleanup.try_with_resource=false +cleanup.unlooped_while=false +cleanup.unreachable_block=false +cleanup.use_anonymous_class_creation=false +cleanup.use_autoboxing=false +cleanup.use_blocks=true +cleanup.use_blocks_only_for_return_and_throw=false +cleanup.use_directly_map_method=false +cleanup.use_lambda=true +cleanup.use_parentheses_in_expressions=false +cleanup.use_string_is_blank=false +cleanup.use_this_for_non_static_field_access=false +cleanup.use_this_for_non_static_field_access_only_if_necessary=true +cleanup.use_this_for_non_static_method_access=false +cleanup.use_this_for_non_static_method_access_only_if_necessary=true +cleanup.use_unboxing=false +cleanup.use_var=false +cleanup.useless_continue=false +cleanup.useless_return=false +cleanup.valueof_rather_than_instantiation=false +cleanup_profile=_CheckStyle-Generated github-copilot-for-eclipse +cleanup_settings_version=2 +eclipse.preferences.version=1 +formatter_profile=_CheckStyle-Generated github-copilot-for-eclipse +formatter_settings_version=23 From 26bbeee7c6a537c5f1bb543537c4cb4ee9f4d0aa Mon Sep 17 00:00:00 2001 From: Sheng Chen Date: Thu, 5 Dec 2024 11:29:21 +0800 Subject: [PATCH 007/690] build - Add Copilot agent (#9) --- .github/workflows/ci.yml | 11 +++++++++ .gitignore | 6 ++++- .../build.properties | 6 ++++- .../copilot-agent/package-lock.json | 24 +++++++++++++++++++ .../copilot-agent/package.json | 17 +++++++++++++ 5 files changed, 62 insertions(+), 2 deletions(-) create mode 100644 com.microsoft.copilot.eclipse.core/copilot-agent/package-lock.json create mode 100644 com.microsoft.copilot.eclipse.core/copilot-agent/package.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 94386370..125b4a99 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,12 +13,23 @@ jobs: steps: - uses: actions/checkout@v4 + + - name: Setup Node.js environment + uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Install Copilot agent + working-directory: com.microsoft.copilot.eclipse.core/copilot-agent + run: npm i + - name: Set up JDK 17 uses: actions/setup-java@v4 with: java-version: '17' distribution: 'temurin' cache: maven + - name: Build with Maven run: ./mvnw clean verify diff --git a/.gitignore b/.gitignore index 6cb923f3..2e352862 100644 --- a/.gitignore +++ b/.gitignore @@ -26,5 +26,9 @@ replay_pid* # Mac files .DS_Store -## Maven +# Maven **/target/ + +# Copilot agent +**/node_modules/ +**/copilot-agent/native/ \ No newline at end of file diff --git a/com.microsoft.copilot.eclipse.core/build.properties b/com.microsoft.copilot.eclipse.core/build.properties index 4c15b450..a19753f5 100644 --- a/com.microsoft.copilot.eclipse.core/build.properties +++ b/com.microsoft.copilot.eclipse.core/build.properties @@ -2,5 +2,9 @@ source.. = src output.. = target/classes bin.includes = META-INF/,\ .,\ - plugin.xml + plugin.xml,\ + copilot-agent/native/darwin-arm64/,\ + copilot-agent/native/darwin-x64/,\ + copilot-agent/native/linux-x64/,\ + copilot-agent/native/win32-x64/ diff --git a/com.microsoft.copilot.eclipse.core/copilot-agent/package-lock.json b/com.microsoft.copilot.eclipse.core/copilot-agent/package-lock.json new file mode 100644 index 00000000..a2b5295b --- /dev/null +++ b/com.microsoft.copilot.eclipse.core/copilot-agent/package-lock.json @@ -0,0 +1,24 @@ +{ + "name": "github-copilot-eclipse", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "github-copilot-eclipse", + "version": "0.0.1", + "hasInstallScript": true, + "dependencies": { + "@github/copilot-language-server": "^1.244.0" + } + }, + "node_modules/@github/copilot-language-server": { + "version": "1.246.0", + "resolved": "https://registry.npmjs.org/@github/copilot-language-server/-/copilot-language-server-1.246.0.tgz", + "integrity": "sha512-THHDhaTs36DGYmi7OO2SZJke0AESBDxDjZYXFfMh+9TqIDAFkIaJiG4A4c6b5hjx3UzcV6pxe1LovKm9sKjJ8Q==", + "bin": { + "copilot-language-server": "dist/language-server.js" + } + } + } +} diff --git a/com.microsoft.copilot.eclipse.core/copilot-agent/package.json b/com.microsoft.copilot.eclipse.core/copilot-agent/package.json new file mode 100644 index 00000000..27e794ea --- /dev/null +++ b/com.microsoft.copilot.eclipse.core/copilot-agent/package.json @@ -0,0 +1,17 @@ +{ + "name": "github-copilot-eclipse", + "version": "0.0.1", + "description": "Sub package for managing @github/copilot-language-server dependency", + "publisher": "GitHub", + "private": true, + "repository": { + "type": "git", + "url": "https://github.com/microsoft/copilot-eclipse.git" + }, + "scripts": { + "postinstall": "npx --yes copyfiles -u 3 'node_modules/@github/copilot-language-server/native/**/*' '.'" + }, + "dependencies": { + "@github/copilot-language-server": "^1.244.0" + } +} From 255982c4493223de120cabcdf818c4b26fd3bf90 Mon Sep 17 00:00:00 2001 From: ethanyhou <149548697+ethanyhou@users.noreply.github.com> Date: Fri, 6 Dec 2024 08:57:22 +0800 Subject: [PATCH 008/690] eng - Added launch configuration for project debug. (#10) --- launch/plugin_debug_configuration.launch | 192 +++++++++++++++++++++++ 1 file changed, 192 insertions(+) create mode 100644 launch/plugin_debug_configuration.launch diff --git a/launch/plugin_debug_configuration.launch b/launch/plugin_debug_configuration.launch new file mode 100644 index 00000000..fec96a04 --- /dev/null +++ b/launch/plugin_debug_configuration.launch @@ -0,0 +1,192 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From edb3c37b9682223e779de16cc8b5d082066c077a Mon Sep 17 00:00:00 2001 From: Sheng Chen Date: Fri, 6 Dec 2024 13:31:11 +0800 Subject: [PATCH 009/690] feat - Initialize Copilot language server on plugin activation (#11) * Activate the GCLS(GitHub Copilot Language Server) when plugin is activated. * Check the local auth result from the GCLS and store the result to the auth status manager. --- .settings/dict | 1 + checkstyle.xml | 3 +- .../.classpath | 11 +++ .../.project | 34 +++++++ .../.settings/org.eclipse.jdt.core.prefs | 9 ++ .../.settings/org.eclipse.m2e.core.prefs | 4 + .../META-INF/MANIFEST.MF | 14 +++ .../build.properties | 5 + .../pom.xml | 17 ++++ .../eclipse/core/AuthStatusManagerTests.java | 41 ++++++++ .../lsp/LsStreamConnectionProviderTests.java | 32 ++++++ .../.settings/org.eclipse.jdt.core.prefs | 2 +- .../META-INF/MANIFEST.MF | 11 ++- com.microsoft.copilot.eclipse.core/plugin.xml | 24 +++++ .../eclipse/core/AuthStatusManager.java | 42 ++++++++ .../copilot/eclipse/core/CopilotCore.java | 84 ++++++++++++++++ .../eclipse/core/CopilotCorePlugin.java | 21 ---- .../core/lsp/CopilotLanguageClient.java | 11 +++ .../core/lsp/CopilotLanguageServer.java | 22 +++++ .../lsp/CopilotLanguageServerConnection.java | 44 +++++++++ .../core/lsp/LsStreamConnectionProvider.java | 99 +++++++++++++++++++ .../core/lsp/protocol/AuthStatusResult.java | 89 +++++++++++++++++ .../core/lsp/protocol/CheckStatusParams.java | 59 +++++++++++ .../lsp/protocol/CopilotCapabilities.java | 66 +++++++++++++ .../lsp/protocol/InitializationOptions.java | 93 +++++++++++++++++ .../core/lsp/protocol/NameAndVersion.java | 75 ++++++++++++++ .../eclipse/core/utils/PlatformUtils.java | 47 +++++++++ .../.settings/org.eclipse.jdt.core.prefs | 2 +- pom.xml | 1 + target-platform.target | 12 +++ 30 files changed, 950 insertions(+), 25 deletions(-) create mode 100644 com.microsoft.copilot.eclipse.core.test/.classpath create mode 100644 com.microsoft.copilot.eclipse.core.test/.project create mode 100644 com.microsoft.copilot.eclipse.core.test/.settings/org.eclipse.jdt.core.prefs create mode 100644 com.microsoft.copilot.eclipse.core.test/.settings/org.eclipse.m2e.core.prefs create mode 100644 com.microsoft.copilot.eclipse.core.test/META-INF/MANIFEST.MF create mode 100644 com.microsoft.copilot.eclipse.core.test/build.properties create mode 100644 com.microsoft.copilot.eclipse.core.test/pom.xml create mode 100644 com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/AuthStatusManagerTests.java create mode 100644 com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/lsp/LsStreamConnectionProviderTests.java create mode 100644 com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/AuthStatusManager.java create mode 100644 com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/CopilotCore.java delete mode 100644 com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/CopilotCorePlugin.java create mode 100644 com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/CopilotLanguageClient.java create mode 100644 com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/CopilotLanguageServer.java create mode 100644 com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/CopilotLanguageServerConnection.java create mode 100644 com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/LsStreamConnectionProvider.java create mode 100644 com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/AuthStatusResult.java create mode 100644 com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/CheckStatusParams.java create mode 100644 com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/CopilotCapabilities.java create mode 100644 com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/InitializationOptions.java create mode 100644 com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/NameAndVersion.java create mode 100644 com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/utils/PlatformUtils.java diff --git a/.settings/dict b/.settings/dict index dc6a5502..dd8d6872 100644 --- a/.settings/dict +++ b/.settings/dict @@ -1,2 +1,3 @@ copilot plugin +telemetry diff --git a/checkstyle.xml b/checkstyle.xml index 24d9ebf3..17ebfac9 100644 --- a/checkstyle.xml +++ b/checkstyle.xml @@ -313,7 +313,8 @@ - + + diff --git a/com.microsoft.copilot.eclipse.core.test/.classpath b/com.microsoft.copilot.eclipse.core.test/.classpath new file mode 100644 index 00000000..38f401ad --- /dev/null +++ b/com.microsoft.copilot.eclipse.core.test/.classpath @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/com.microsoft.copilot.eclipse.core.test/.project b/com.microsoft.copilot.eclipse.core.test/.project new file mode 100644 index 00000000..4a7bc7b5 --- /dev/null +++ b/com.microsoft.copilot.eclipse.core.test/.project @@ -0,0 +1,34 @@ + + + com.microsoft.copilot.eclipse.core.test + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.pde.ManifestBuilder + + + + + org.eclipse.pde.SchemaBuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.m2e.core.maven2Nature + org.eclipse.pde.PluginNature + org.eclipse.jdt.core.javanature + + diff --git a/com.microsoft.copilot.eclipse.core.test/.settings/org.eclipse.jdt.core.prefs b/com.microsoft.copilot.eclipse.core.test/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 00000000..62ef3488 --- /dev/null +++ b/com.microsoft.copilot.eclipse.core.test/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,9 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.targetPlatform=17 +org.eclipse.jdt.core.compiler.compliance=17 +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=warning +org.eclipse.jdt.core.compiler.release=enabled +org.eclipse.jdt.core.compiler.source=17 diff --git a/com.microsoft.copilot.eclipse.core.test/.settings/org.eclipse.m2e.core.prefs b/com.microsoft.copilot.eclipse.core.test/.settings/org.eclipse.m2e.core.prefs new file mode 100644 index 00000000..f897a7f1 --- /dev/null +++ b/com.microsoft.copilot.eclipse.core.test/.settings/org.eclipse.m2e.core.prefs @@ -0,0 +1,4 @@ +activeProfiles= +eclipse.preferences.version=1 +resolveWorkspaceProjects=true +version=1 diff --git a/com.microsoft.copilot.eclipse.core.test/META-INF/MANIFEST.MF b/com.microsoft.copilot.eclipse.core.test/META-INF/MANIFEST.MF new file mode 100644 index 00000000..3443b426 --- /dev/null +++ b/com.microsoft.copilot.eclipse.core.test/META-INF/MANIFEST.MF @@ -0,0 +1,14 @@ +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Name: com.microsoft.copilot.eclipse.core.test +Bundle-SymbolicName: com.microsoft.copilot.eclipse.core.test;singleton:=true +Bundle-Version: 0.1.0.qualifier +Bundle-RequiredExecutionEnvironment: JavaSE-17 +Automatic-Module-Name: com.microsoft.copilot.eclipse.core.test +Import-Package: org.objenesis;version="[3.4.0,4.0.0)", + org.osgi.framework;version="[1.10.0,2.0.0)" +Require-Bundle: com.microsoft.copilot.eclipse.core;bundle-version="0.1.0", + org.mockito.mockito-core;bundle-version="5.14.2", + org.junit;bundle-version="4.13.2", + org.eclipse.lsp4e;bundle-version="0.18.12", + org.eclipse.jdt.annotation;bundle-version="2.3.0" diff --git a/com.microsoft.copilot.eclipse.core.test/build.properties b/com.microsoft.copilot.eclipse.core.test/build.properties new file mode 100644 index 00000000..100b378a --- /dev/null +++ b/com.microsoft.copilot.eclipse.core.test/build.properties @@ -0,0 +1,5 @@ +source.. = src +output.. = target/classes +bin.includes = META-INF/,\ + . + diff --git a/com.microsoft.copilot.eclipse.core.test/pom.xml b/com.microsoft.copilot.eclipse.core.test/pom.xml new file mode 100644 index 00000000..3f28a5d2 --- /dev/null +++ b/com.microsoft.copilot.eclipse.core.test/pom.xml @@ -0,0 +1,17 @@ + + 4.0.0 + + com.microsoft.copilot.eclipse + github-copilot-for-eclipse + 0.1.0-SNAPSHOT + + com.microsoft.copilot.eclipse.core.test + eclipse-test-plugin + ${base.name} :: Core Tests + + + true + + \ No newline at end of file diff --git a/com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/AuthStatusManagerTests.java b/com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/AuthStatusManagerTests.java new file mode 100644 index 00000000..22663f06 --- /dev/null +++ b/com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/AuthStatusManagerTests.java @@ -0,0 +1,41 @@ +package com.microsoft.copilot.eclipse.core; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.when; + +import java.util.concurrent.CompletableFuture; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import com.microsoft.copilot.eclipse.core.lsp.CopilotLanguageServerConnection; +import com.microsoft.copilot.eclipse.core.lsp.protocol.AuthStatusResult; + +@RunWith(MockitoJUnitRunner.class) +public class AuthStatusManagerTests { + + @Mock + CopilotLanguageServerConnection mockConnection; + AuthStatusManager authStatusManager; + + @Before + public void setUp() { + authStatusManager = new AuthStatusManager(mockConnection); + } + + @Test + public void testAuthStatusResultOnSuccess() { + AuthStatusResult expectedResult = new AuthStatusResult(); + expectedResult.setStatus(AuthStatusResult.OK); + when(mockConnection.checkStatus(false)).thenReturn(CompletableFuture.completedFuture(expectedResult)); + + authStatusManager.checkStatus(); + + assertEquals(AuthStatusResult.OK, authStatusManager.getAuthStatusResult().getStatus()); + + } + +} diff --git a/com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/lsp/LsStreamConnectionProviderTests.java b/com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/lsp/LsStreamConnectionProviderTests.java new file mode 100644 index 00000000..1b7e96bd --- /dev/null +++ b/com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/lsp/LsStreamConnectionProviderTests.java @@ -0,0 +1,32 @@ +package com.microsoft.copilot.eclipse.core.lsp; + +import static org.junit.Assert.assertEquals; + +import java.io.IOException; + +import org.junit.Test; + +import com.microsoft.copilot.eclipse.core.lsp.protocol.InitializationOptions; + +public class LsStreamConnectionProviderTests { + + @Test + public void testInitializationOptions() { + LsStreamConnectionProvider provider = new LsStreamConnectionProvider(); + + InitializationOptions options = (InitializationOptions) provider.getInitializationOptions(null); + + assertEquals(LsStreamConnectionProvider.EDITOR_NAME, options.getEditorInfo().getName()); + assertEquals(LsStreamConnectionProvider.EDITOR_PLUGIN_NAME, options.getEditorPluginInfo().getName()); + } + + @Test + public void testStartLanguageServer() throws IOException { + LsStreamConnectionProvider provider = new LsStreamConnectionProvider(); + try { + provider.start(); + } finally { + provider.stop(); + } + } +} diff --git a/com.microsoft.copilot.eclipse.core/.settings/org.eclipse.jdt.core.prefs b/com.microsoft.copilot.eclipse.core/.settings/org.eclipse.jdt.core.prefs index c42a96bf..aa5854b6 100644 --- a/com.microsoft.copilot.eclipse.core/.settings/org.eclipse.jdt.core.prefs +++ b/com.microsoft.copilot.eclipse.core/.settings/org.eclipse.jdt.core.prefs @@ -150,7 +150,7 @@ org.eclipse.jdt.core.formatter.insert_new_line_at_end_of_file_if_missing=do not org.eclipse.jdt.core.formatter.insert_new_line_before_catch_in_try_statement=do not insert org.eclipse.jdt.core.formatter.insert_new_line_before_closing_brace_in_array_initializer=do not insert org.eclipse.jdt.core.formatter.insert_new_line_before_else_in_if_statement=do not insert -org.eclipse.jdt.core.formatter.insert_new_line_before_finally_in_try_statement=insert +org.eclipse.jdt.core.formatter.insert_new_line_before_finally_in_try_statement=do not insert org.eclipse.jdt.core.formatter.insert_new_line_before_while_in_do_statement=do not insert org.eclipse.jdt.core.formatter.insert_space_after_additive_operator=insert org.eclipse.jdt.core.formatter.insert_space_after_and_in_type_parameter=insert diff --git a/com.microsoft.copilot.eclipse.core/META-INF/MANIFEST.MF b/com.microsoft.copilot.eclipse.core/META-INF/MANIFEST.MF index e92232fc..ea216d43 100644 --- a/com.microsoft.copilot.eclipse.core/META-INF/MANIFEST.MF +++ b/com.microsoft.copilot.eclipse.core/META-INF/MANIFEST.MF @@ -3,8 +3,17 @@ Bundle-ManifestVersion: 2 Bundle-Name: com.microsoft.copilot.eclipse.core Bundle-SymbolicName: com.microsoft.copilot.eclipse.core;singleton:=true Bundle-Version: 0.1.0.qualifier -Bundle-Activator: com.microsoft.copilot.eclipse.core.CopilotCorePlugin +Export-Package: com.microsoft.copilot.eclipse.core, + com.microsoft.copilot.eclipse.core.lsp, + com.microsoft.copilot.eclipse.core.lsp.protocol +Bundle-Activator: com.microsoft.copilot.eclipse.core.CopilotCore Bundle-RequiredExecutionEnvironment: JavaSE-17 Automatic-Module-Name: com.microsoft.copilot.eclipse.core Bundle-ActivationPolicy: lazy Import-Package: org.osgi.framework;version="[1.10.0,2.0.0)" +Require-Bundle: org.eclipse.lsp4e;bundle-version="0.18.12", + org.eclipse.core.runtime;bundle-version="3.31.100", + org.eclipse.lsp4j;bundle-version="0.23.1", + org.eclipse.lsp4j.jsonrpc;bundle-version="0.23.1", + org.apache.commons.lang3;bundle-version="3.17.0", + org.eclipse.jdt.annotation;bundle-version="2.3.0" diff --git a/com.microsoft.copilot.eclipse.core/plugin.xml b/com.microsoft.copilot.eclipse.core/plugin.xml index f422d55d..a55a9fda 100644 --- a/com.microsoft.copilot.eclipse.core/plugin.xml +++ b/com.microsoft.copilot.eclipse.core/plugin.xml @@ -1,4 +1,28 @@ + + + + + + + + + diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/AuthStatusManager.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/AuthStatusManager.java new file mode 100644 index 00000000..22cb4e6e --- /dev/null +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/AuthStatusManager.java @@ -0,0 +1,42 @@ +package com.microsoft.copilot.eclipse.core; + +import com.microsoft.copilot.eclipse.core.lsp.CopilotLanguageServerConnection; +import com.microsoft.copilot.eclipse.core.lsp.protocol.AuthStatusResult; + +/** + * Manager for the authentication status. + */ +public class AuthStatusManager { + + private CopilotLanguageServerConnection connection; + + private AuthStatusResult authStatusResult; + + /** + * Constructor for the AuthStatusManager. + * + * @param connection the connection to the language server. + */ + public AuthStatusManager(CopilotLanguageServerConnection connection) { + this.connection = connection; + this.authStatusResult = new AuthStatusResult(); + this.authStatusResult.setStatus(AuthStatusResult.NOT_SIGNED_IN); + } + + /** + * Check the login status for current machine. + */ + public void checkStatus() { + this.connection.checkStatus(false).thenAccept(result -> { + this.authStatusResult = result; + }).exceptionally(ex -> { + // TODO: log & send telemetry + this.authStatusResult.setStatus(AuthStatusResult.ERROR); + return null; + }); + } + + public AuthStatusResult getAuthStatusResult() { + return authStatusResult; + } +} diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/CopilotCore.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/CopilotCore.java new file mode 100644 index 00000000..0dd2cd8d --- /dev/null +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/CopilotCore.java @@ -0,0 +1,84 @@ +package com.microsoft.copilot.eclipse.core; + +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Plugin; +import org.eclipse.core.runtime.Status; +import org.eclipse.core.runtime.jobs.Job; +import org.eclipse.lsp4e.LanguageServerWrapper; +import org.eclipse.lsp4e.LanguageServersRegistry; +import org.eclipse.lsp4e.LanguageServiceAccessor; +import org.osgi.framework.BundleContext; + +import com.microsoft.copilot.eclipse.core.lsp.CopilotLanguageServerConnection; + +/** + * Activator class for the Copilot core plugin. + */ +public class CopilotCore extends Plugin { + + private CopilotLanguageServerConnection copilotLanguageServer; + private AuthStatusManager authStatusManager; + + private static CopilotCore COPILOT_CORE_PLUGIN = null; + + /** + * Creates the Copilot core plugin. The plugin is created automatically by the Eclipse framework. Clients must not + * call this constructor. + */ + public CopilotCore() { + super(); + COPILOT_CORE_PLUGIN = this; + } + + public static Plugin getPlugin() { + return COPILOT_CORE_PLUGIN; + } + + @Override + public void start(BundleContext context) throws Exception { + init(); + } + + @Override + public void stop(BundleContext context) throws Exception { + + } + + @SuppressWarnings("restriction") + void init() { + final Runnable initRunnable = () -> { + LanguageServersRegistry.LanguageServerDefinition serverDef = LanguageServersRegistry.getInstance() + .getDefinition(CopilotLanguageServerConnection.SERVER_ID); + if (serverDef == null) { + // TODO: log & send telemetry + throw new IllegalStateException( + "Language server definition not found for " + CopilotLanguageServerConnection.SERVER_ID); + } + + LanguageServerWrapper wrapper = LanguageServiceAccessor.startLanguageServer(serverDef); + this.copilotLanguageServer = new CopilotLanguageServerConnection(wrapper); + this.authStatusManager = new AuthStatusManager(this.copilotLanguageServer); + + this.authStatusManager.checkStatus(); + }; + + Job initJob = new Job("GitHub Copilot Initialization...") { + protected IStatus run(IProgressMonitor monitor) { + initRunnable.run(); + return Status.OK_STATUS; + } + }; + initJob.setUser(true); + initJob.schedule(); + } + + public CopilotLanguageServerConnection getCopilotLanguageServer() { + return copilotLanguageServer; + } + + public AuthStatusManager getAuthStatusManager() { + return authStatusManager; + } + +} diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/CopilotCorePlugin.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/CopilotCorePlugin.java deleted file mode 100644 index c6ef7af5..00000000 --- a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/CopilotCorePlugin.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.microsoft.copilot.eclipse.core; - -import org.osgi.framework.BundleActivator; -import org.osgi.framework.BundleContext; - -/** - * Activator class for the Copilot core plugin. - */ -public class CopilotCorePlugin implements BundleActivator { - - @Override - public void start(BundleContext context) throws Exception { - - } - - @Override - public void stop(BundleContext context) throws Exception { - - } - -} diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/CopilotLanguageClient.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/CopilotLanguageClient.java new file mode 100644 index 00000000..699fff08 --- /dev/null +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/CopilotLanguageClient.java @@ -0,0 +1,11 @@ +package com.microsoft.copilot.eclipse.core.lsp; + +import org.eclipse.lsp4e.LanguageClientImpl; + +/** + * Language client for the Copilot language server. + */ +@SuppressWarnings("restriction") +public class CopilotLanguageClient extends LanguageClientImpl { + +} diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/CopilotLanguageServer.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/CopilotLanguageServer.java new file mode 100644 index 00000000..1945d79f --- /dev/null +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/CopilotLanguageServer.java @@ -0,0 +1,22 @@ +package com.microsoft.copilot.eclipse.core.lsp; + +import java.util.concurrent.CompletableFuture; + +import org.eclipse.lsp4j.jsonrpc.services.JsonRequest; +import org.eclipse.lsp4j.services.LanguageServer; + +import com.microsoft.copilot.eclipse.core.lsp.protocol.AuthStatusResult; +import com.microsoft.copilot.eclipse.core.lsp.protocol.CheckStatusParams; + +/** + * Interface for Copilot Language Server. + */ +public interface CopilotLanguageServer extends LanguageServer { + + /** + * Check the login status for current machine. + */ + @JsonRequest + CompletableFuture checkStatus(CheckStatusParams param); + +} diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/CopilotLanguageServerConnection.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/CopilotLanguageServerConnection.java new file mode 100644 index 00000000..eab1a722 --- /dev/null +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/CopilotLanguageServerConnection.java @@ -0,0 +1,44 @@ +package com.microsoft.copilot.eclipse.core.lsp; + +import java.util.concurrent.CompletableFuture; +import java.util.function.Function; + +import org.eclipse.lsp4e.LanguageServerWrapper; +import org.eclipse.lsp4j.services.LanguageServer; + +import com.microsoft.copilot.eclipse.core.lsp.protocol.AuthStatusResult; +import com.microsoft.copilot.eclipse.core.lsp.protocol.CheckStatusParams; + +/** + * Language Server for Copilot agent. + */ +@SuppressWarnings("restriction") +public class CopilotLanguageServerConnection { + + public static final String SERVER_ID = "com.microsoft.copilot.eclipse.ls"; + + private LanguageServerWrapper languageServerWrapper; + + /** + * Constructor for the CopilotLanguageServer. + * + * @param languageServerWrapper the language server wrapper. + */ + public CopilotLanguageServerConnection(LanguageServerWrapper languageServerWrapper) { + this.languageServerWrapper = languageServerWrapper; + } + + /** + * Check the login status for current machine. + */ + @SuppressWarnings("null") + public CompletableFuture checkStatus(Boolean localCheckOnly) { + Function> fn = server -> { + CheckStatusParams param = new CheckStatusParams(); + param.setLocalChecksOnly(localCheckOnly); + return ((CopilotLanguageServer) server).checkStatus(param); + }; + return this.languageServerWrapper.execute(fn); + } + +} diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/LsStreamConnectionProvider.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/LsStreamConnectionProvider.java new file mode 100644 index 00000000..4c3daf39 --- /dev/null +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/LsStreamConnectionProvider.java @@ -0,0 +1,99 @@ +package com.microsoft.copilot.eclipse.core.lsp; + +import java.io.File; +import java.io.IOException; +import java.net.URI; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.LinkedList; +import java.util.List; + +import org.eclipse.core.runtime.FileLocator; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.lsp4e.server.ProcessStreamConnectionProvider; +import org.osgi.framework.Bundle; + +import com.microsoft.copilot.eclipse.core.CopilotCore; +import com.microsoft.copilot.eclipse.core.lsp.protocol.CopilotCapabilities; +import com.microsoft.copilot.eclipse.core.lsp.protocol.InitializationOptions; +import com.microsoft.copilot.eclipse.core.lsp.protocol.NameAndVersion; +import com.microsoft.copilot.eclipse.core.utils.PlatformUtils; + +/** + * Stream connection provider for the Copilot language server. + */ +public class LsStreamConnectionProvider extends ProcessStreamConnectionProvider { + + public static final String EDITOR_NAME = "Eclipse"; + + public static final String EDITOR_PLUGIN_NAME = "GotHub Copilot for Eclipse"; + + @Override + public Object getInitializationOptions(@Nullable URI rootUri) { + NameAndVersion editorInfo = new NameAndVersion(EDITOR_NAME, PlatformUtils.getEclipseVersion()); + Bundle bundle = CopilotCore.getPlugin().getBundle(); + String bundleVersion = bundle == null ? "unknown" : bundle.getVersion().toString(); + NameAndVersion editorPluginInfo = new NameAndVersion(EDITOR_PLUGIN_NAME, bundleVersion); + CopilotCapabilities capabilities = new CopilotCapabilities(false, false); + return new InitializationOptions(editorInfo, editorPluginInfo, capabilities); + } + + @Override + public void start() throws IOException { + Path binary = findBinary(); + if (binary == null) { + throw new IOException("Could not find the language server binary"); + } + + File executable = binary.toFile(); + if (!executable.canExecute()) { + boolean canExecute = executable.setExecutable(true); + if (!canExecute) { + // TODO: throw error or handle it? + } + } + List commands = new LinkedList<>(); + commands.add(binary.toString()); + commands.add("--stdio"); + this.setCommands(commands); + // TODO: will it have permission issue on Unix based OS? + super.start(); + } + + private @Nullable Path findBinary() throws IOException { + if (PlatformUtils.isMac() && PlatformUtils.isIntel64()) { + return null; + } + + Path binDir = findAgentBinaryDirectoryPath(); + if (binDir == null) { + return null; + } + + Path executable = null; + if (PlatformUtils.isLinux()) { + executable = binDir.resolve("linux-x64/copilot-language-server"); + } else if (PlatformUtils.isWindows()) { + executable = binDir.resolve("win32-x64/copilot-language-server.exe"); + } else if (PlatformUtils.isMac()) { + if (PlatformUtils.isArm64()) { + executable = binDir.resolve("darwin-arm64/copilot-language-server"); + } else { + executable = binDir.resolve("darwin-x64/copilot-language-server"); + } + } + + return executable != null && Files.exists(executable) ? executable : null; + } + + private @Nullable Path findAgentBinaryDirectoryPath() throws IOException { + URL url = CopilotCore.getPlugin().getBundle().getEntry("copilot-agent/native"); + if (url == null) { + return null; + } + + return Path.of(FileLocator.toFileURL(url).getPath()); + } + +} diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/AuthStatusResult.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/AuthStatusResult.java new file mode 100644 index 00000000..37c95e28 --- /dev/null +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/AuthStatusResult.java @@ -0,0 +1,89 @@ +package com.microsoft.copilot.eclipse.core.lsp.protocol; + +import java.util.Objects; + +import org.apache.commons.lang3.StringUtils; +import org.eclipse.lsp4j.jsonrpc.util.ToStringBuilder; +import org.eclipse.lsp4j.jsonrpc.validation.NonNull; + +/** + * Result for the Authentication status. + */ +public class AuthStatusResult { + + public static final String OK = "OK"; + public static final String ERROR = "Error"; + public static final String WARNING = "Warning"; + public static final String NOT_SIGNED_IN = "NotSignedIn"; + public static final String NOT_AUTHORIZED = "NotAuthorized"; + + @NonNull + private String status; + + private String user; + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public String getUser() { + return user; + } + + public void setUser(String user) { + this.user = user; + } + + public boolean isSignedIn() { + return OK.equals(this.status) && StringUtils.isNotEmpty(this.user); + } + + public boolean isNotSignedIn() { + return NOT_SIGNED_IN.equals(this.status); + } + + public boolean isWarning() { + return WARNING.equals(this.status); + } + + public boolean isError() { + return ERROR.equals(this.status); + } + + public boolean isNotAuthorized() { + return NOT_AUTHORIZED.equals(this.status); + } + + @Override + public int hashCode() { + return Objects.hash(status, user); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + AuthStatusResult other = (AuthStatusResult) obj; + return Objects.equals(status, other.status) && Objects.equals(user, other.user); + } + + @Override + public String toString() { + ToStringBuilder builder = new ToStringBuilder(this); + builder.add("status", status); + builder.add("user", user); + return builder.toString(); + } + +} diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/CheckStatusParams.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/CheckStatusParams.java new file mode 100644 index 00000000..43b11091 --- /dev/null +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/CheckStatusParams.java @@ -0,0 +1,59 @@ +package com.microsoft.copilot.eclipse.core.lsp.protocol; + +import java.util.Objects; + +import org.eclipse.lsp4j.jsonrpc.util.ToStringBuilder; + +/** + * Parameter used for the checkStatus request. + */ +public class CheckStatusParams { + + private boolean localChecksOnly; + + private boolean forceRefresh; + + public boolean isLocalChecksOnly() { + return localChecksOnly; + } + + public void setLocalChecksOnly(boolean localChecksOnly) { + this.localChecksOnly = localChecksOnly; + } + + public boolean isForceRefresh() { + return forceRefresh; + } + + public void setForceRefresh(boolean forceRefresh) { + this.forceRefresh = forceRefresh; + } + + @Override + public int hashCode() { + return Objects.hash(forceRefresh, localChecksOnly); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + CheckStatusParams other = (CheckStatusParams) obj; + return forceRefresh == other.forceRefresh && localChecksOnly == other.localChecksOnly; + } + + @Override + public String toString() { + ToStringBuilder builder = new ToStringBuilder(this); + builder.add("localChecksOnly", localChecksOnly); + builder.add("forceRefresh", forceRefresh); + return builder.toString(); + } +} diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/CopilotCapabilities.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/CopilotCapabilities.java new file mode 100644 index 00000000..ad4b32c7 --- /dev/null +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/CopilotCapabilities.java @@ -0,0 +1,66 @@ +package com.microsoft.copilot.eclipse.core.lsp.protocol; + +import java.util.Objects; + +import org.eclipse.lsp4j.jsonrpc.util.ToStringBuilder; + +/** + * Capabilities of the Copilot language server. + */ +public class CopilotCapabilities { + private boolean fetch; + + private boolean watchedFiles; + + /** + * Creates a new CopilotCapabilities. + */ + public CopilotCapabilities(boolean fetch, boolean watchedFiles) { + this.fetch = fetch; + this.watchedFiles = watchedFiles; + } + + public boolean isFetch() { + return fetch; + } + + public void setFetch(boolean fetch) { + this.fetch = fetch; + } + + public boolean isWatchedFiles() { + return watchedFiles; + } + + public void setWatchedFiles(boolean watchedFiles) { + this.watchedFiles = watchedFiles; + } + + @Override + public String toString() { + ToStringBuilder builder = new ToStringBuilder(this); + builder.add("fetch", fetch); + builder.add("watchedFiles", watchedFiles); + return builder.toString(); + } + + @Override + public int hashCode() { + return Objects.hash(fetch, watchedFiles); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + CopilotCapabilities other = (CopilotCapabilities) obj; + return fetch == other.fetch && watchedFiles == other.watchedFiles; + } +} diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/InitializationOptions.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/InitializationOptions.java new file mode 100644 index 00000000..4e1ab16c --- /dev/null +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/InitializationOptions.java @@ -0,0 +1,93 @@ +package com.microsoft.copilot.eclipse.core.lsp.protocol; + +import java.util.Objects; + +import org.eclipse.lsp4j.jsonrpc.util.ToStringBuilder; +import org.eclipse.lsp4j.jsonrpc.validation.NonNull; + +/** + * Initialization options for the Copilot language server. + */ +public class InitializationOptions { + @NonNull + private NameAndVersion editorInfo; + + @NonNull + private NameAndVersion editorPluginInfo; + + private CopilotCapabilities capabilities; + + /** + * Creates a new InitializationOptions. + */ + public InitializationOptions(NameAndVersion editorInfo, NameAndVersion editorPluginInfo) { + this.editorInfo = editorInfo; + this.editorPluginInfo = editorPluginInfo; + } + + /** + * Creates a new InitializationOptions. + */ + public InitializationOptions(NameAndVersion editorInfo, NameAndVersion editorPluginInfo, + CopilotCapabilities capabilities) { + this.editorInfo = editorInfo; + this.editorPluginInfo = editorPluginInfo; + this.capabilities = capabilities; + } + + @NonNull + public NameAndVersion getEditorInfo() { + return editorInfo; + } + + public void setEditorInfo(@NonNull NameAndVersion editorInfo) { + this.editorInfo = editorInfo; + } + + @NonNull + public NameAndVersion getEditorPluginInfo() { + return editorPluginInfo; + } + + public void setEditorPluginInfo(@NonNull NameAndVersion editorPluginInfo) { + this.editorPluginInfo = editorPluginInfo; + } + + public CopilotCapabilities getCapabilities() { + return capabilities; + } + + public void setCapabilities(CopilotCapabilities capabilities) { + this.capabilities = capabilities; + } + + @Override + public String toString() { + ToStringBuilder builder = new ToStringBuilder(this); + builder.add("editorInfo", editorInfo); + builder.add("editorPluginInfo", editorPluginInfo); + builder.add("capabilities", capabilities); + return builder.toString(); + } + + @Override + public int hashCode() { + return Objects.hash(capabilities, editorInfo, editorPluginInfo); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + InitializationOptions other = (InitializationOptions) obj; + return Objects.equals(capabilities, other.capabilities) && Objects.equals(editorInfo, other.editorInfo) + && Objects.equals(editorPluginInfo, other.editorPluginInfo); + } +} diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/NameAndVersion.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/NameAndVersion.java new file mode 100644 index 00000000..b0a0d9c0 --- /dev/null +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/NameAndVersion.java @@ -0,0 +1,75 @@ +package com.microsoft.copilot.eclipse.core.lsp.protocol; + +import java.util.Objects; + +import org.eclipse.lsp4j.jsonrpc.util.Preconditions; +import org.eclipse.lsp4j.jsonrpc.util.ToStringBuilder; +import org.eclipse.lsp4j.jsonrpc.validation.NonNull; + +/** + * A name and version pair. + */ +public class NameAndVersion { + @NonNull + private String name; + + @NonNull + private String version; + + /** + * Creates a new NameAndVersion. + * + * @param name the name. + * @param version the version. + */ + public NameAndVersion(@NonNull String name, @NonNull String version) { + this.name = Preconditions.checkNotNull(name, "name"); + this.version = Preconditions.checkNotNull(version, "version"); + } + + @NonNull + public String getName() { + return name; + } + + public void setName(@NonNull String name) { + this.name = name; + } + + @NonNull + public String getVersion() { + return version; + } + + public void setVersion(@NonNull String version) { + this.version = version; + } + + @Override + public String toString() { + ToStringBuilder b = new ToStringBuilder(this); + b.add("name", name); + b.add("version", version); + return b.toString(); + } + + @Override + public int hashCode() { + return Objects.hash(name, version); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + NameAndVersion other = (NameAndVersion) obj; + return Objects.equals(name, other.name) && Objects.equals(version, other.version); + } +} diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/utils/PlatformUtils.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/utils/PlatformUtils.java new file mode 100644 index 00000000..9168986e --- /dev/null +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/utils/PlatformUtils.java @@ -0,0 +1,47 @@ +package com.microsoft.copilot.eclipse.core.utils; + +import org.eclipse.core.runtime.Platform; +import org.osgi.framework.Bundle; + +/** + * Utility class for platform related operations. + */ +public class PlatformUtils { + + public static final String EC_PLATFORM_BUNDLE_NAME = "org.eclipse.platform"; + + private PlatformUtils() { + } + + /** + * Get the version of the Eclipse platform. + */ + public static String getEclipseVersion() { + Bundle bundle = Platform.getBundle(EC_PLATFORM_BUNDLE_NAME); + if (bundle == null) { + return "unknown"; + } + return bundle.getVersion().toString(); + } + + public static boolean isMac() { + return Platform.getOS().equals(Platform.OS_MACOSX); + } + + public static boolean isLinux() { + return Platform.getOS().equals(Platform.OS_LINUX); + } + + public static boolean isWindows() { + return Platform.getOS().equals(Platform.OS_WIN32); + } + + public static boolean isIntel64() { + return Platform.getOSArch().equals(Platform.ARCH_X86_64); + } + + public static boolean isArm64() { + return Platform.getOSArch().equals(Platform.ARCH_AARCH64); + } + +} diff --git a/com.microsoft.copilot.eclipse.ui/.settings/org.eclipse.jdt.core.prefs b/com.microsoft.copilot.eclipse.ui/.settings/org.eclipse.jdt.core.prefs index c42a96bf..aa5854b6 100644 --- a/com.microsoft.copilot.eclipse.ui/.settings/org.eclipse.jdt.core.prefs +++ b/com.microsoft.copilot.eclipse.ui/.settings/org.eclipse.jdt.core.prefs @@ -150,7 +150,7 @@ org.eclipse.jdt.core.formatter.insert_new_line_at_end_of_file_if_missing=do not org.eclipse.jdt.core.formatter.insert_new_line_before_catch_in_try_statement=do not insert org.eclipse.jdt.core.formatter.insert_new_line_before_closing_brace_in_array_initializer=do not insert org.eclipse.jdt.core.formatter.insert_new_line_before_else_in_if_statement=do not insert -org.eclipse.jdt.core.formatter.insert_new_line_before_finally_in_try_statement=insert +org.eclipse.jdt.core.formatter.insert_new_line_before_finally_in_try_statement=do not insert org.eclipse.jdt.core.formatter.insert_new_line_before_while_in_do_statement=do not insert org.eclipse.jdt.core.formatter.insert_space_after_additive_operator=insert org.eclipse.jdt.core.formatter.insert_space_after_and_in_type_parameter=insert diff --git a/pom.xml b/pom.xml index 6a6090b3..90293ef3 100644 --- a/pom.xml +++ b/pom.xml @@ -18,6 +18,7 @@ com.microsoft.copilot.eclipse.core + com.microsoft.copilot.eclipse.core.test com.microsoft.copilot.eclipse.ui diff --git a/target-platform.target b/target-platform.target index e29da0bc..8fac5a31 100644 --- a/target-platform.target +++ b/target-platform.target @@ -10,6 +10,8 @@ + + @@ -34,6 +36,16 @@ rxjava 3.1.10 + + org.mockito + mockito-core + 5.14.2 + + + org.objenesis + objenesis + 3.4 + From da57b7ca2d091cd6cad43a939be05f0579643010 Mon Sep 17 00:00:00 2001 From: Sheng Chen Date: Fri, 6 Dec 2024 15:20:53 +0800 Subject: [PATCH 010/690] eng - Update issue templates and code owners (#12) --- .github/CODEOWNERS | 1 + .github/ISSUE_TEMPLATE/bug_report.md | 32 +++++++++++++++++++++++ .github/ISSUE_TEMPLATE/feature_request.md | 20 ++++++++++++++ 3 files changed, 53 insertions(+) create mode 100644 .github/CODEOWNERS create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000..67e6be67 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @jdneo @ethanyhou @yanshudan \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..28db62a1 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,32 @@ +--- +name: Bug report +about: Create a report to help us improve +title: "[Bug] " +labels: bug +assignees: '' + +--- + +**Environment** +- OS: +- Eclipse Version: +- Plugin Version: + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 00000000..54c28deb --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: "[Feat] " +labels: enhancement +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. From 669e5cc59fc489c3c162028258df05db890bbd0d Mon Sep 17 00:00:00 2001 From: Sheng Chen Date: Fri, 6 Dec 2024 16:03:27 +0800 Subject: [PATCH 011/690] test - Migrate test framework to JUnit 5 (#13) --- .../META-INF/MANIFEST.MF | 5 +++-- .../eclipse/core/AuthStatusManagerTests.java | 18 +++++++++--------- .../lsp/LsStreamConnectionProviderTests.java | 10 +++++----- .../copilot/eclipse/core/CopilotCore.java | 4 +++- .../lsp/CopilotLanguageServerConnection.java | 7 +++++++ target-platform.target | 8 +++++++- 6 files changed, 34 insertions(+), 18 deletions(-) diff --git a/com.microsoft.copilot.eclipse.core.test/META-INF/MANIFEST.MF b/com.microsoft.copilot.eclipse.core.test/META-INF/MANIFEST.MF index 3443b426..2112db88 100644 --- a/com.microsoft.copilot.eclipse.core.test/META-INF/MANIFEST.MF +++ b/com.microsoft.copilot.eclipse.core.test/META-INF/MANIFEST.MF @@ -9,6 +9,7 @@ Import-Package: org.objenesis;version="[3.4.0,4.0.0)", org.osgi.framework;version="[1.10.0,2.0.0)" Require-Bundle: com.microsoft.copilot.eclipse.core;bundle-version="0.1.0", org.mockito.mockito-core;bundle-version="5.14.2", - org.junit;bundle-version="4.13.2", org.eclipse.lsp4e;bundle-version="0.18.12", - org.eclipse.jdt.annotation;bundle-version="2.3.0" + org.eclipse.jdt.annotation;bundle-version="2.3.0", + junit-jupiter-api;bundle-version="5.11.0", + org.mockito.junit-jupiter;bundle-version="5.14.2" diff --git a/com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/AuthStatusManagerTests.java b/com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/AuthStatusManagerTests.java index 22663f06..596e8416 100644 --- a/com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/AuthStatusManagerTests.java +++ b/com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/AuthStatusManagerTests.java @@ -1,33 +1,33 @@ package com.microsoft.copilot.eclipse.core; -import static org.junit.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.Mockito.when; import java.util.concurrent.CompletableFuture; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; -import org.mockito.junit.MockitoJUnitRunner; +import org.mockito.junit.jupiter.MockitoExtension; import com.microsoft.copilot.eclipse.core.lsp.CopilotLanguageServerConnection; import com.microsoft.copilot.eclipse.core.lsp.protocol.AuthStatusResult; -@RunWith(MockitoJUnitRunner.class) -public class AuthStatusManagerTests { +@ExtendWith(MockitoExtension.class) +class AuthStatusManagerTests { @Mock CopilotLanguageServerConnection mockConnection; AuthStatusManager authStatusManager; - @Before + @BeforeEach public void setUp() { authStatusManager = new AuthStatusManager(mockConnection); } @Test - public void testAuthStatusResultOnSuccess() { + void testAuthStatusResultOnSuccess() { AuthStatusResult expectedResult = new AuthStatusResult(); expectedResult.setStatus(AuthStatusResult.OK); when(mockConnection.checkStatus(false)).thenReturn(CompletableFuture.completedFuture(expectedResult)); diff --git a/com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/lsp/LsStreamConnectionProviderTests.java b/com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/lsp/LsStreamConnectionProviderTests.java index 1b7e96bd..f866d4ea 100644 --- a/com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/lsp/LsStreamConnectionProviderTests.java +++ b/com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/lsp/LsStreamConnectionProviderTests.java @@ -1,17 +1,17 @@ package com.microsoft.copilot.eclipse.core.lsp; -import static org.junit.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; import java.io.IOException; -import org.junit.Test; +import org.junit.jupiter.api.Test; import com.microsoft.copilot.eclipse.core.lsp.protocol.InitializationOptions; -public class LsStreamConnectionProviderTests { +class LsStreamConnectionProviderTests { @Test - public void testInitializationOptions() { + void testInitializationOptions() { LsStreamConnectionProvider provider = new LsStreamConnectionProvider(); InitializationOptions options = (InitializationOptions) provider.getInitializationOptions(null); @@ -21,7 +21,7 @@ public void testInitializationOptions() { } @Test - public void testStartLanguageServer() throws IOException { + void testStartLanguageServer() throws IOException { LsStreamConnectionProvider provider = new LsStreamConnectionProvider(); try { provider.start(); diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/CopilotCore.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/CopilotCore.java index 0dd2cd8d..907771d2 100644 --- a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/CopilotCore.java +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/CopilotCore.java @@ -42,7 +42,9 @@ public void start(BundleContext context) throws Exception { @Override public void stop(BundleContext context) throws Exception { - + if (copilotLanguageServer != null) { + copilotLanguageServer.stop(); + } } @SuppressWarnings("restriction") diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/CopilotLanguageServerConnection.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/CopilotLanguageServerConnection.java index eab1a722..c257c36b 100644 --- a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/CopilotLanguageServerConnection.java +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/CopilotLanguageServerConnection.java @@ -41,4 +41,11 @@ public CompletableFuture checkStatus(Boolean localCheckOnly) { return this.languageServerWrapper.execute(fn); } + /** + * Stop the language server. + */ + public void stop() { + this.languageServerWrapper.stop(); + } + } diff --git a/target-platform.target b/target-platform.target index 8fac5a31..220747c4 100644 --- a/target-platform.target +++ b/target-platform.target @@ -11,7 +11,7 @@ - + @@ -42,6 +42,12 @@ 5.14.2 + org.mockito + mockito-junit-jupiter + 5.14.2 + + + org.objenesis objenesis 3.4 From f52f4f0b357379fc888604359a985e87fd69b583 Mon Sep 17 00:00:00 2001 From: Sheng Chen Date: Mon, 9 Dec 2024 13:27:15 +0800 Subject: [PATCH 012/690] feat - Early startup the Core plugin from UI plugin (#14) --- .../.classpath | 11 ++++++ .../.project | 34 +++++++++++++++++++ .../.settings/org.eclipse.jdt.core.prefs | 9 +++++ .../.settings/org.eclipse.m2e.core.prefs | 4 +++ .../META-INF/MANIFEST.MF | 17 ++++++++++ .../build.properties | 5 +++ com.microsoft.copilot.eclipse.ui.test/pom.xml | 17 ++++++++++ .../copilot/eclipse/ui/CopilotUiTests.java | 22 ++++++++++++ .../META-INF/MANIFEST.MF | 6 ++-- com.microsoft.copilot.eclipse.ui/plugin.xml | 6 ++++ .../{CopilotUiPlugin.java => CopilotUi.java} | 7 ++-- .../microsoft/copilot/eclipse/ui/StartUp.java | 15 ++++++++ pom.xml | 5 ++- 13 files changed, 153 insertions(+), 5 deletions(-) create mode 100644 com.microsoft.copilot.eclipse.ui.test/.classpath create mode 100644 com.microsoft.copilot.eclipse.ui.test/.project create mode 100644 com.microsoft.copilot.eclipse.ui.test/.settings/org.eclipse.jdt.core.prefs create mode 100644 com.microsoft.copilot.eclipse.ui.test/.settings/org.eclipse.m2e.core.prefs create mode 100644 com.microsoft.copilot.eclipse.ui.test/META-INF/MANIFEST.MF create mode 100644 com.microsoft.copilot.eclipse.ui.test/build.properties create mode 100644 com.microsoft.copilot.eclipse.ui.test/pom.xml create mode 100644 com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/CopilotUiTests.java rename com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/{CopilotUiPlugin.java => CopilotUi.java} (63%) create mode 100644 com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/StartUp.java diff --git a/com.microsoft.copilot.eclipse.ui.test/.classpath b/com.microsoft.copilot.eclipse.ui.test/.classpath new file mode 100644 index 00000000..38f401ad --- /dev/null +++ b/com.microsoft.copilot.eclipse.ui.test/.classpath @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/com.microsoft.copilot.eclipse.ui.test/.project b/com.microsoft.copilot.eclipse.ui.test/.project new file mode 100644 index 00000000..03fbd5a9 --- /dev/null +++ b/com.microsoft.copilot.eclipse.ui.test/.project @@ -0,0 +1,34 @@ + + + com.microsoft.copilot.eclipse.ui.test + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + org.eclipse.pde.ManifestBuilder + + + + + org.eclipse.pde.SchemaBuilder + + + + + + org.eclipse.m2e.core.maven2Nature + org.eclipse.pde.PluginNature + org.eclipse.jdt.core.javanature + + diff --git a/com.microsoft.copilot.eclipse.ui.test/.settings/org.eclipse.jdt.core.prefs b/com.microsoft.copilot.eclipse.ui.test/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 00000000..62ef3488 --- /dev/null +++ b/com.microsoft.copilot.eclipse.ui.test/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,9 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.targetPlatform=17 +org.eclipse.jdt.core.compiler.compliance=17 +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=warning +org.eclipse.jdt.core.compiler.release=enabled +org.eclipse.jdt.core.compiler.source=17 diff --git a/com.microsoft.copilot.eclipse.ui.test/.settings/org.eclipse.m2e.core.prefs b/com.microsoft.copilot.eclipse.ui.test/.settings/org.eclipse.m2e.core.prefs new file mode 100644 index 00000000..f897a7f1 --- /dev/null +++ b/com.microsoft.copilot.eclipse.ui.test/.settings/org.eclipse.m2e.core.prefs @@ -0,0 +1,4 @@ +activeProfiles= +eclipse.preferences.version=1 +resolveWorkspaceProjects=true +version=1 diff --git a/com.microsoft.copilot.eclipse.ui.test/META-INF/MANIFEST.MF b/com.microsoft.copilot.eclipse.ui.test/META-INF/MANIFEST.MF new file mode 100644 index 00000000..7d1c3ea9 --- /dev/null +++ b/com.microsoft.copilot.eclipse.ui.test/META-INF/MANIFEST.MF @@ -0,0 +1,17 @@ +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Name: com.microsoft.copilot.eclipse.ui.test +Bundle-SymbolicName: com.microsoft.copilot.eclipse.ui.test;singleton:=true +Bundle-Version: 0.1.0.qualifier +Bundle-RequiredExecutionEnvironment: JavaSE-17 +Automatic-Module-Name: com.microsoft.copilot.eclipse.ui.test +Import-Package: org.objenesis;version="[3.4.0,4.0.0)", + org.osgi.framework;version="[1.10.0,2.0.0)" +Require-Bundle: org.mockito.mockito-core;bundle-version="5.14.2", + org.eclipse.lsp4e;bundle-version="0.18.12", + org.eclipse.jdt.annotation;bundle-version="2.3.0", + junit-jupiter-api;bundle-version="5.11.0", + org.mockito.junit-jupiter;bundle-version="5.14.2", + com.microsoft.copilot.eclipse.core;bundle-version="0.1.0", + com.microsoft.copilot.eclipse.ui;bundle-version="0.1.0", + org.eclipse.core.runtime;bundle-version="3.31.100" diff --git a/com.microsoft.copilot.eclipse.ui.test/build.properties b/com.microsoft.copilot.eclipse.ui.test/build.properties new file mode 100644 index 00000000..100b378a --- /dev/null +++ b/com.microsoft.copilot.eclipse.ui.test/build.properties @@ -0,0 +1,5 @@ +source.. = src +output.. = target/classes +bin.includes = META-INF/,\ + . + diff --git a/com.microsoft.copilot.eclipse.ui.test/pom.xml b/com.microsoft.copilot.eclipse.ui.test/pom.xml new file mode 100644 index 00000000..6c98c002 --- /dev/null +++ b/com.microsoft.copilot.eclipse.ui.test/pom.xml @@ -0,0 +1,17 @@ + + 4.0.0 + + com.microsoft.copilot.eclipse + github-copilot-for-eclipse + 0.1.0-SNAPSHOT + + com.microsoft.copilot.eclipse.ui.test + eclipse-plugin + ${base.name} :: UI Tests + + + true + + \ No newline at end of file diff --git a/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/CopilotUiTests.java b/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/CopilotUiTests.java new file mode 100644 index 00000000..14409d29 --- /dev/null +++ b/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/CopilotUiTests.java @@ -0,0 +1,22 @@ +package com.microsoft.copilot.eclipse.ui; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import org.eclipse.core.runtime.Platform; +import org.junit.jupiter.api.Test; +import org.osgi.framework.Bundle; + +class CopilotUiTests { + + @Test + void testCopilotCoreWakeUp() throws Exception { + CopilotUi ui = new CopilotUi(); + ui.start(null); + + Bundle bundle = Platform.getBundle("com.microsoft.copilot.eclipse.core"); + + assertNotNull(bundle); + assertEquals(Bundle.ACTIVE, bundle.getState()); + } +} diff --git a/com.microsoft.copilot.eclipse.ui/META-INF/MANIFEST.MF b/com.microsoft.copilot.eclipse.ui/META-INF/MANIFEST.MF index b46c543d..47b63666 100644 --- a/com.microsoft.copilot.eclipse.ui/META-INF/MANIFEST.MF +++ b/com.microsoft.copilot.eclipse.ui/META-INF/MANIFEST.MF @@ -3,9 +3,11 @@ Bundle-ManifestVersion: 2 Bundle-Name: com.microsoft.copilot.eclipse.ui Bundle-SymbolicName: com.microsoft.copilot.eclipse.ui;singleton:=true Bundle-Version: 0.1.0.qualifier -Bundle-Activator: com.microsoft.copilot.eclipse.ui.CopilotUiPlugin +Export-Package: com.microsoft.copilot.eclipse.ui +Bundle-Activator: com.microsoft.copilot.eclipse.ui.CopilotUi Bundle-RequiredExecutionEnvironment: JavaSE-17 Automatic-Module-Name: com.microsoft.copilot.eclipse.ui Bundle-ActivationPolicy: lazy Import-Package: org.osgi.framework;version="[1.10.0,2.0.0)" -Require-Bundle: com.microsoft.copilot.eclipse.core;bundle-version="0.1.0" +Require-Bundle: com.microsoft.copilot.eclipse.core;bundle-version="0.1.0", + org.eclipse.ui.ide diff --git a/com.microsoft.copilot.eclipse.ui/plugin.xml b/com.microsoft.copilot.eclipse.ui/plugin.xml index f422d55d..44ad4023 100644 --- a/com.microsoft.copilot.eclipse.ui/plugin.xml +++ b/com.microsoft.copilot.eclipse.ui/plugin.xml @@ -1,4 +1,10 @@ + + + + diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/CopilotUiPlugin.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/CopilotUi.java similarity index 63% rename from com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/CopilotUiPlugin.java rename to com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/CopilotUi.java index 9a0a59cd..b13f40f9 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/CopilotUiPlugin.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/CopilotUi.java @@ -3,14 +3,17 @@ import org.osgi.framework.BundleActivator; import org.osgi.framework.BundleContext; +import com.microsoft.copilot.eclipse.core.CopilotCore; + /** * Activator class for the Copilot UI plugin. */ -public class CopilotUiPlugin implements BundleActivator { +public class CopilotUi implements BundleActivator { @Override public void start(BundleContext context) throws Exception { - + // wake up the Core plugin by calling a method from it. + CopilotCore.getPlugin(); } @Override diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/StartUp.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/StartUp.java new file mode 100644 index 00000000..e53d37ee --- /dev/null +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/StartUp.java @@ -0,0 +1,15 @@ +package com.microsoft.copilot.eclipse.ui; + +import org.eclipse.ui.IStartup; + +/** + * Early startup the Copilot for Eclipse plugin. + */ +public class StartUp implements IStartup { + + @Override + public void earlyStartup() { + // do nothing. + } + +} diff --git a/pom.xml b/pom.xml index 90293ef3..1ff5f28c 100644 --- a/pom.xml +++ b/pom.xml @@ -18,8 +18,11 @@ com.microsoft.copilot.eclipse.core - com.microsoft.copilot.eclipse.core.test com.microsoft.copilot.eclipse.ui + + + com.microsoft.copilot.eclipse.core.test + com.microsoft.copilot.eclipse.ui.test From 06d77761b84fe4d3c5896d92e7e08e4d23dd1bd7 Mon Sep 17 00:00:00 2001 From: Sheng Chen Date: Mon, 9 Dec 2024 16:31:38 +0800 Subject: [PATCH 013/690] build - Add Mac and Windows for CI (#15) --- .github/workflows/ci.yml | 5 ++++- .../copilot-agent/package.json | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 125b4a99..c3274a63 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,7 +9,10 @@ on: jobs: build: - runs-on: ubuntu-latest + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] steps: - uses: actions/checkout@v4 diff --git a/com.microsoft.copilot.eclipse.core/copilot-agent/package.json b/com.microsoft.copilot.eclipse.core/copilot-agent/package.json index 27e794ea..0c16fe6a 100644 --- a/com.microsoft.copilot.eclipse.core/copilot-agent/package.json +++ b/com.microsoft.copilot.eclipse.core/copilot-agent/package.json @@ -9,7 +9,7 @@ "url": "https://github.com/microsoft/copilot-eclipse.git" }, "scripts": { - "postinstall": "npx --yes copyfiles -u 3 'node_modules/@github/copilot-language-server/native/**/*' '.'" + "postinstall": "npx --yes copyfiles -u 3 \"node_modules/@github/copilot-language-server/native/**/*\" \".\"" }, "dependencies": { "@github/copilot-language-server": "^1.244.0" From 5df8208425d6cac114be914dd7c0762762799375 Mon Sep 17 00:00:00 2001 From: ethanyhou <149548697+ethanyhou@users.noreply.github.com> Date: Tue, 10 Dec 2024 10:06:09 +0800 Subject: [PATCH 014/690] fix - Updated the parsing method to get the agent across platforms. (#17) --- .../eclipse/core/lsp/LsStreamConnectionProvider.java | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/LsStreamConnectionProvider.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/LsStreamConnectionProvider.java index 4c3daf39..bdb88ec2 100644 --- a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/LsStreamConnectionProvider.java +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/LsStreamConnectionProvider.java @@ -3,6 +3,7 @@ import java.io.File; import java.io.IOException; import java.net.URI; +import java.net.URISyntaxException; import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; @@ -10,6 +11,7 @@ import java.util.List; import org.eclipse.core.runtime.FileLocator; +import org.eclipse.core.runtime.URIUtil; import org.eclipse.jdt.annotation.Nullable; import org.eclipse.lsp4e.server.ProcessStreamConnectionProvider; import org.osgi.framework.Bundle; @@ -93,7 +95,12 @@ public void start() throws IOException { return null; } - return Path.of(FileLocator.toFileURL(url).getPath()); + try { + return URIUtil.toFile(URIUtil.toURI(FileLocator.toFileURL(url))).toPath(); + } catch (URISyntaxException | IOException e) { + // TODO: Log exception via telemetry. + return null; + } } } From ac55bd5f06f2ba810abc9651de196e6f8465446c Mon Sep 17 00:00:00 2001 From: Sheng Chen Date: Tue, 10 Dec 2024 15:47:22 +0800 Subject: [PATCH 015/690] feat - Initialize the editor lifecycle listeners for completion (#19) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .settings/dict | 1 + .../META-INF/MANIFEST.MF | 3 +- .../copilot/eclipse/core/CopilotCore.java | 2 +- .../lsp/CopilotLanguageServerConnection.java | 28 ++++- .../core/lsp/LsStreamConnectionProvider.java | 1 - .../META-INF/MANIFEST.MF | 6 +- .../ui/completion/EditorManagerTests.java | 35 ++++++ .../META-INF/MANIFEST.MF | 12 ++- .../copilot/eclipse/ui/CopilotUi.java | 64 ++++++++++- .../ui/completion/CompletionHandler.java | 102 ++++++++++++++++++ .../completion/EditorLifecycleListener.java | 63 +++++++++++ .../eclipse/ui/completion/EditorsManager.java | 60 +++++++++++ .../copilot/eclipse/ui/utils/SwtUtils.java | 71 ++++++++++++ .../copilot/eclipse/ui/utils/UiUtils.java | 34 ++++++ 14 files changed, 472 insertions(+), 10 deletions(-) create mode 100644 com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/completion/EditorManagerTests.java create mode 100644 com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionHandler.java create mode 100644 com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/EditorLifecycleListener.java create mode 100644 com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/EditorsManager.java create mode 100644 com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/utils/SwtUtils.java create mode 100644 com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/utils/UiUtils.java diff --git a/.settings/dict b/.settings/dict index dd8d6872..a003f6fd 100644 --- a/.settings/dict +++ b/.settings/dict @@ -1,3 +1,4 @@ copilot plugin telemetry +lifecycle diff --git a/com.microsoft.copilot.eclipse.core/META-INF/MANIFEST.MF b/com.microsoft.copilot.eclipse.core/META-INF/MANIFEST.MF index ea216d43..ab0e2d6d 100644 --- a/com.microsoft.copilot.eclipse.core/META-INF/MANIFEST.MF +++ b/com.microsoft.copilot.eclipse.core/META-INF/MANIFEST.MF @@ -16,4 +16,5 @@ Require-Bundle: org.eclipse.lsp4e;bundle-version="0.18.12", org.eclipse.lsp4j;bundle-version="0.23.1", org.eclipse.lsp4j.jsonrpc;bundle-version="0.23.1", org.apache.commons.lang3;bundle-version="3.17.0", - org.eclipse.jdt.annotation;bundle-version="2.3.0" + org.eclipse.jdt.annotation;bundle-version="2.3.0", + org.eclipse.jface.text diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/CopilotCore.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/CopilotCore.java index 907771d2..dbdf1297 100644 --- a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/CopilotCore.java +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/CopilotCore.java @@ -31,7 +31,7 @@ public CopilotCore() { COPILOT_CORE_PLUGIN = this; } - public static Plugin getPlugin() { + public static CopilotCore getPlugin() { return COPILOT_CORE_PLUGIN; } diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/CopilotLanguageServerConnection.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/CopilotLanguageServerConnection.java index c257c36b..0268b230 100644 --- a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/CopilotLanguageServerConnection.java +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/CopilotLanguageServerConnection.java @@ -1,8 +1,11 @@ package com.microsoft.copilot.eclipse.core.lsp; +import java.io.IOException; +import java.net.URI; import java.util.concurrent.CompletableFuture; import java.util.function.Function; +import org.eclipse.jface.text.IDocument; import org.eclipse.lsp4e.LanguageServerWrapper; import org.eclipse.lsp4j.services.LanguageServer; @@ -12,7 +15,7 @@ /** * Language Server for Copilot agent. */ -@SuppressWarnings("restriction") +@SuppressWarnings({ "restriction", "null" }) public class CopilotLanguageServerConnection { public static final String SERVER_ID = "com.microsoft.copilot.eclipse.ls"; @@ -28,10 +31,31 @@ public CopilotLanguageServerConnection(LanguageServerWrapper languageServerWrapp this.languageServerWrapper = languageServerWrapper; } + /** + * Connect the document to the language server. The LSP4E will take care of all the document lifecycle events after + * that. + */ + public void connectDocument(IDocument document) throws IOException { + this.languageServerWrapper.connectDocument(document); + } + + /** + * Disconnect the document from the language server. + */ + public void disconnectDocument(URI uri) { + this.languageServerWrapper.disconnect(uri); + } + + /** + * Get the document version for the given URI. + */ + public int getDocumentVersion(URI uri) { + return this.languageServerWrapper.getTextDocumentVersion(uri); + } + /** * Check the login status for current machine. */ - @SuppressWarnings("null") public CompletableFuture checkStatus(Boolean localCheckOnly) { Function> fn = server -> { CheckStatusParams param = new CheckStatusParams(); diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/LsStreamConnectionProvider.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/LsStreamConnectionProvider.java index bdb88ec2..48ba83c5 100644 --- a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/LsStreamConnectionProvider.java +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/LsStreamConnectionProvider.java @@ -59,7 +59,6 @@ public void start() throws IOException { commands.add(binary.toString()); commands.add("--stdio"); this.setCommands(commands); - // TODO: will it have permission issue on Unix based OS? super.start(); } diff --git a/com.microsoft.copilot.eclipse.ui.test/META-INF/MANIFEST.MF b/com.microsoft.copilot.eclipse.ui.test/META-INF/MANIFEST.MF index 7d1c3ea9..15589da4 100644 --- a/com.microsoft.copilot.eclipse.ui.test/META-INF/MANIFEST.MF +++ b/com.microsoft.copilot.eclipse.ui.test/META-INF/MANIFEST.MF @@ -14,4 +14,8 @@ Require-Bundle: org.mockito.mockito-core;bundle-version="5.14.2", org.mockito.junit-jupiter;bundle-version="5.14.2", com.microsoft.copilot.eclipse.core;bundle-version="0.1.0", com.microsoft.copilot.eclipse.ui;bundle-version="0.1.0", - org.eclipse.core.runtime;bundle-version="3.31.100" + org.eclipse.core.runtime;bundle-version="3.31.100", + org.eclipse.ui;bundle-version="3.206.100", + org.eclipse.ui.ide, + org.eclipse.ui.workbench.texteditor, + org.eclipse.jface.text;bundle-version="3.25.200" diff --git a/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/completion/EditorManagerTests.java b/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/completion/EditorManagerTests.java new file mode 100644 index 00000000..de006db2 --- /dev/null +++ b/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/completion/EditorManagerTests.java @@ -0,0 +1,35 @@ +package com.microsoft.copilot.eclipse.ui.completion; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.Mockito.mock; + +import org.eclipse.ui.texteditor.ITextEditor; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; + +import com.microsoft.copilot.eclipse.core.lsp.CopilotLanguageServerConnection; + +@ExtendWith(MockitoExtension.class) +class EditorManagerTests { + + @Test + void testCreateHandlerForNull() { + CopilotLanguageServerConnection mockServer = mock(CopilotLanguageServerConnection.class); + EditorsManager manager = new EditorsManager(mockServer); + + assertNull(manager.getOrCreateCompletionHandlerFor(null)); + } + + @Test + void getOrCreateCompletionHandlerForReturnsNewHandlerWhenNotPresent() { + ITextEditor mockEditor = mock(ITextEditor.class); + CopilotLanguageServerConnection mockServer = mock(CopilotLanguageServerConnection.class); + + EditorsManager manager = new EditorsManager(mockServer); + CompletionHandler handler = manager.getOrCreateCompletionHandlerFor(mockEditor); + + assertNotNull(handler); + } +} diff --git a/com.microsoft.copilot.eclipse.ui/META-INF/MANIFEST.MF b/com.microsoft.copilot.eclipse.ui/META-INF/MANIFEST.MF index 47b63666..b1a4e699 100644 --- a/com.microsoft.copilot.eclipse.ui/META-INF/MANIFEST.MF +++ b/com.microsoft.copilot.eclipse.ui/META-INF/MANIFEST.MF @@ -3,11 +3,19 @@ Bundle-ManifestVersion: 2 Bundle-Name: com.microsoft.copilot.eclipse.ui Bundle-SymbolicName: com.microsoft.copilot.eclipse.ui;singleton:=true Bundle-Version: 0.1.0.qualifier -Export-Package: com.microsoft.copilot.eclipse.ui +Export-Package: com.microsoft.copilot.eclipse.ui, + com.microsoft.copilot.eclipse.ui.completion Bundle-Activator: com.microsoft.copilot.eclipse.ui.CopilotUi Bundle-RequiredExecutionEnvironment: JavaSE-17 Automatic-Module-Name: com.microsoft.copilot.eclipse.ui Bundle-ActivationPolicy: lazy Import-Package: org.osgi.framework;version="[1.10.0,2.0.0)" Require-Bundle: com.microsoft.copilot.eclipse.core;bundle-version="0.1.0", - org.eclipse.ui.ide + org.eclipse.ui.ide, + org.eclipse.ui.workbench.texteditor, + org.eclipse.ui;bundle-version="3.206.100", + org.eclipse.jface.text;bundle-version="3.25.200", + org.eclipse.core.runtime;bundle-version="3.31.100", + org.eclipse.jdt.annotation, + org.eclipse.core.resources, + org.eclipse.lsp4e diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/CopilotUi.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/CopilotUi.java index b13f40f9..6c0a0d1d 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/CopilotUi.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/CopilotUi.java @@ -1,24 +1,84 @@ package com.microsoft.copilot.eclipse.ui; +import org.eclipse.ui.IEditorPart; +import org.eclipse.ui.IWorkbenchWindow; +import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.texteditor.ITextEditor; import org.osgi.framework.BundleActivator; import org.osgi.framework.BundleContext; import com.microsoft.copilot.eclipse.core.CopilotCore; +import com.microsoft.copilot.eclipse.core.lsp.CopilotLanguageServerConnection; +import com.microsoft.copilot.eclipse.ui.completion.EditorLifecycleListener; +import com.microsoft.copilot.eclipse.ui.completion.EditorsManager; +import com.microsoft.copilot.eclipse.ui.utils.SwtUtils; /** * Activator class for the Copilot UI plugin. */ public class CopilotUi implements BundleActivator { + private static final int RETRY_COUNT = 30; + private EditorLifecycleListener editorLifecycleListener; + private EditorsManager editorsManager; + @Override public void start(BundleContext context) throws Exception { - // wake up the Core plugin by calling a method from it. - CopilotCore.getPlugin(); + // wake up Core plugin and wait until copilot LS is ready + // TODO: check if we can improve logic here, for example, use a listener to wait for LS ready. + CopilotLanguageServerConnection connection = null; + for (int i = 0; i < RETRY_COUNT; i++) { + connection = CopilotCore.getPlugin().getCopilotLanguageServer(); + if (connection != null) { + break; + } + Thread.sleep(1000); + } + if (connection == null) { + // TODO: log & send telemetry + throw new IllegalStateException("Copilot language server is not ready."); + } + + this.editorsManager = new EditorsManager(connection); + this.editorLifecycleListener = new EditorLifecycleListener(editorsManager); + + registerPartListener(); + + // Initialize the completion handler for the active editor in case we miss the event + // to initialize it. + initComletionHandlerForActiveEditor(); } @Override public void stop(BundleContext context) throws Exception { + unregisterPartListener(); + if (this.editorsManager != null) { + this.editorsManager.dispose(); + } + } + + private void registerPartListener() { + IWorkbenchWindow[] windows = PlatformUI.getWorkbench().getWorkbenchWindows(); + for (IWorkbenchWindow window : windows) { + window.getPartService().addPartListener(this.editorLifecycleListener); + } + } + + private void initComletionHandlerForActiveEditor() { + IEditorPart editorPart = SwtUtils.getActiveEditorPart(); + if (editorPart != null) { + ITextEditor textEditor = editorPart.getAdapter(ITextEditor.class); + if (textEditor != null) { + this.editorsManager.getOrCreateCompletionHandlerFor(textEditor); + } + } + } + private void unregisterPartListener() { + IWorkbenchWindow[] windows = PlatformUI.getWorkbench().getWorkbenchWindows(); + for (IWorkbenchWindow window : windows) { + window.getPartService().removePartListener(this.editorLifecycleListener); + } } } diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionHandler.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionHandler.java new file mode 100644 index 00000000..7ef0ace3 --- /dev/null +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionHandler.java @@ -0,0 +1,102 @@ +package com.microsoft.copilot.eclipse.ui.completion; + +import java.io.IOException; +import java.net.URI; + +import org.eclipse.jface.text.IDocument; +import org.eclipse.jface.text.ITextListener; +import org.eclipse.jface.text.ITextOperationTarget; +import org.eclipse.jface.text.ITextViewer; +import org.eclipse.jface.text.TextEvent; +import org.eclipse.lsp4e.LSPEclipseUtils; +import org.eclipse.swt.custom.CaretEvent; +import org.eclipse.swt.custom.CaretListener; +import org.eclipse.ui.texteditor.ITextEditor; + +import com.microsoft.copilot.eclipse.core.lsp.CopilotLanguageServerConnection; +import com.microsoft.copilot.eclipse.ui.utils.SwtUtils; +import com.microsoft.copilot.eclipse.ui.utils.UiUtils; + +/** + * Handle completion for an ITextEditor. + */ +public class CompletionHandler implements ITextListener, CaretListener { + + private CopilotLanguageServerConnection lsConnection; + private ITextEditor editor; + private ITextViewer textViewer; + private IDocument document; + private URI documentUri; + private int documentVersion; + + /** + * Creates a new completion handler. + */ + public CompletionHandler(CopilotLanguageServerConnection lsConnection, ITextEditor editor) { + this.lsConnection = lsConnection; + this.editor = editor; + this.textViewer = (ITextViewer) this.editor.getAdapter(ITextOperationTarget.class); + this.document = LSPEclipseUtils.getDocument(editor); + this.documentUri = UiUtils.getUriFromTextEditor(editor); + this.documentVersion = 0; + try { + lsConnection.connectDocument(this.document); + } catch (IOException e) { + // TODO: log & send telemetry + return; + } + registerListeners(); + } + + @Override + public void caretMoved(CaretEvent event) { + // it's guaranteed that the document change event comes earlier than caret + // change event. See org.eclipse.swt.custom.StyledText#modifyContent() + int currentVersion = this.lsConnection.getDocumentVersion(this.documentUri); + if (currentVersion == this.documentVersion) { + // if the caret position is changed without document version change, we should remove the ghost text. + // TODO: remove ghost text + } else { + this.documentVersion = currentVersion; + // TODO: trigger completion + } + + } + + @Override + public void textChanged(TextEvent event) { + // this event comes earlier than caret change event. So we should check if the typed characters + // are the same as the ghost. Then determine whether a re-redering or a new completion + // request is needed. + // TODO: check changed text + } + + /** + * Disposes the resources of this completion handler. + */ + public void dispose() { + lsConnection.disconnectDocument(this.documentUri); + if (this.textViewer != null) { + SwtUtils.invokeOnDisplayThread(() -> { + this.textViewer.getTextWidget().removeCaretListener(this); + this.textViewer.removeTextListener(this); + }); + } + + } + + void registerListeners() { + // if the text viewer is null, we will not register listeners. + // the side effect is that the completion will not be triggered for this editor. + if (this.textViewer == null) { + // TODO: log & send telemetry + return; + } + + SwtUtils.invokeOnDisplayThread(() -> { + this.textViewer.getTextWidget().addCaretListener(this); + this.textViewer.addTextListener(this); + }); + } + +} diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/EditorLifecycleListener.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/EditorLifecycleListener.java new file mode 100644 index 00000000..4aba250d --- /dev/null +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/EditorLifecycleListener.java @@ -0,0 +1,63 @@ +package com.microsoft.copilot.eclipse.ui.completion; + +import org.eclipse.ui.IPartListener; +import org.eclipse.ui.IWorkbenchPart; +import org.eclipse.ui.texteditor.ITextEditor; + +/** + * Listen to the lifecycle event of an editor parts. + */ +public class EditorLifecycleListener implements IPartListener { + + private EditorsManager manager; + + /** + * Creates a new EditorLifecycleListener. + */ + public EditorLifecycleListener(EditorsManager manager) { + this.manager = manager; + } + + @Override + public void partActivated(IWorkbenchPart part) { + createCompletionHandlerFor(part); + + } + + @Override + public void partBroughtToTop(IWorkbenchPart part) { + // do nothing. + } + + @Override + public void partClosed(IWorkbenchPart part) { + disposeCompletionHandlerFor(part); + } + + @Override + public void partDeactivated(IWorkbenchPart part) { + // do nothing. + } + + @Override + public void partOpened(IWorkbenchPart part) { + // do nothing. + } + + void createCompletionHandlerFor(IWorkbenchPart part) { + ITextEditor editor = part.getAdapter(ITextEditor.class); + if (editor == null) { + return; + } + manager.getOrCreateCompletionHandlerFor(editor); + } + + void disposeCompletionHandlerFor(IWorkbenchPart part) { + ITextEditor editor = part.getAdapter(ITextEditor.class); + if (editor == null) { + return; + } + manager.disposeCompletionHandlerFor(editor); + } + +} diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/EditorsManager.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/EditorsManager.java new file mode 100644 index 00000000..f38e408c --- /dev/null +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/EditorsManager.java @@ -0,0 +1,60 @@ +package com.microsoft.copilot.eclipse.ui.completion; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.eclipse.ui.texteditor.ITextEditor; + +import com.microsoft.copilot.eclipse.core.lsp.CopilotLanguageServerConnection; + +/** + * Manages the completion handlers for all available ITextEditors. + */ +public class EditorsManager { + + private CopilotLanguageServerConnection languageServer; + private Map editorMap; + + /** + * Creates a new EditorManager. + */ + public EditorsManager(CopilotLanguageServerConnection languageServer) { + this.languageServer = languageServer; + this.editorMap = new ConcurrentHashMap<>(); + } + + /** + * Gets the {@link com.microsoft.copilot.eclipse.ui.completion.CompletionHandler CompletionHandler} for the given + * ITextEditor. If it does not exist, a new one will be created. Returns null if the editor is + * null. + */ + public CompletionHandler getOrCreateCompletionHandlerFor(ITextEditor editor) { + if (editor == null) { + return null; + } + + return editorMap.computeIfAbsent(editor, edt -> new CompletionHandler(this.languageServer, edt)); + } + + /** + * Disposes the {@link com.microsoft.copilot.eclipse.ui.completion.CompletionHandler CompletionHandler} for the given + * ITextEditor. + */ + public void disposeCompletionHandlerFor(ITextEditor editor) { + CompletionHandler handler = editorMap.remove(editor); + if (handler != null) { + handler.dispose(); + } + } + + /** + * Dispose all the handlers. + */ + public void dispose() { + for (CompletionHandler handler : this.editorMap.values()) { + handler.dispose(); + } + this.editorMap.clear(); + } + +} diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/utils/SwtUtils.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/utils/SwtUtils.java new file mode 100644 index 00000000..b745f8ed --- /dev/null +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/utils/SwtUtils.java @@ -0,0 +1,71 @@ +package com.microsoft.copilot.eclipse.ui.utils; + +import java.util.Objects; +import java.util.concurrent.atomic.AtomicReference; + +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Display; +import org.eclipse.ui.IEditorPart; +import org.eclipse.ui.IWorkbench; +import org.eclipse.ui.IWorkbenchWindow; +import org.eclipse.ui.PlatformUI; + +/** + * Utilities for SWT. * + */ +public class SwtUtils { + + private SwtUtils() { + // prevent instantiation + } + + /** + * Invokes the given runnable on the display thread. + */ + public static void invokeOnDisplayThread(Runnable runnable) { + IWorkbench workbench = PlatformUI.getWorkbench(); + IWorkbenchWindow[] windows = workbench.getWorkbenchWindows(); + if (windows != null && windows.length > 0) { + Display display = windows[0].getShell().getDisplay(); + display.syncExec(runnable); + } else { + runnable.run(); + } + } + + /** + * Invokes the given runnable on the display thread. + * + * @param runnable the runnable to invoke + * @param control the control used for the display + */ + public static void invokeOnDisplayThread(Runnable runnable, Control control) { + if (Objects.isNull(control)) { + invokeOnDisplayThread(runnable); + } else { + Display display = control.getDisplay(); + if (display.getThread() == Thread.currentThread()) { + runnable.run(); + } else { + display.syncExec(runnable); + } + } + } + + /** + * Get the active editor part from workbench. + */ + @Nullable + public static IEditorPart getActiveEditorPart() { + AtomicReference ref = new AtomicReference<>(); + invokeOnDisplayThread(() -> { + IWorkbench workbench = PlatformUI.getWorkbench(); + IWorkbenchWindow window = workbench.getActiveWorkbenchWindow(); + if (window != null) { + ref.set(window.getActivePage().getActiveEditor()); + } + }); + return ref.get(); + } +} diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/utils/UiUtils.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/utils/UiUtils.java new file mode 100644 index 00000000..2f609300 --- /dev/null +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/utils/UiUtils.java @@ -0,0 +1,34 @@ +package com.microsoft.copilot.eclipse.ui.utils; + +import java.net.URI; + +import org.eclipse.core.resources.IFile; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.ui.IEditorInput; +import org.eclipse.ui.IFileEditorInput; +import org.eclipse.ui.texteditor.ITextEditor; + +/** + * Utilities for Eclipse UI. + */ +public class UiUtils { + + private UiUtils() { + // prevent instantiation + } + + /** + * Gets the URI of the file opened in the given text editor. + */ + @Nullable + public static URI getUriFromTextEditor(ITextEditor editor) { + IEditorInput input = editor.getEditorInput(); + if (input instanceof IFileEditorInput fileInput) { + IFile file = fileInput.getFile(); + return file.getLocationURI(); + } + + return null; + } + +} From 6f2f67e925cc6aede193f21f530eea9425a21c38 Mon Sep 17 00:00:00 2001 From: ethanyhou <149548697+ethanyhou@users.noreply.github.com> Date: Wed, 11 Dec 2024 13:09:21 +0800 Subject: [PATCH 016/690] feat - Added draft scaffold for the GitHub copilot menu. (#21) --- .../icons/copilot.png | Bin 0 -> 707 bytes com.microsoft.copilot.eclipse.ui/plugin.xml | 51 ++++++++++++++++ .../copilot/eclipse/ui/Constants.java | 9 +++ .../ui/handlers/ShowStatusBarMenuHandler.java | 55 ++++++++++++++++++ .../copilot/eclipse/ui/utils/UiUtils.java | 21 +++++++ 5 files changed, 136 insertions(+) create mode 100644 com.microsoft.copilot.eclipse.ui/icons/copilot.png create mode 100644 com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/Constants.java create mode 100644 com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/ShowStatusBarMenuHandler.java diff --git a/com.microsoft.copilot.eclipse.ui/icons/copilot.png b/com.microsoft.copilot.eclipse.ui/icons/copilot.png new file mode 100644 index 0000000000000000000000000000000000000000..31996ba7e667e0ea2d414b353a40a12ce7c41e53 GIT binary patch literal 707 zcmV;!0zCbRP)tAoU2Wys7|J>9NOJ9dQM-OW-MPk6goMey~7J%cNcYt8AM|)c$G1S zoftr0Ba4bGb$~IBZ6C?e6S2KSl%pBY))c3;Kc1=;$VV zPN#hn%Fa)wPc78eHiD5aXGT%FG)9?_KWBm~W-CRaB{^SqP>W??hM9$?vnx_wE?8V| zp^%KSuUmVitjpK9)!Q*~h(w8&6yDux#p#UH>RynYY_r-|z%y5Sm0NwPSi32St-5rw zZEKU!VRo2%oVmWy*|F5hY>TC&!@qf%t<~k~(dO&x_xeedq-CqfQK!5>n4v0Mc4Wua zW~0)I&&zJp*cjsAd)M$~m&8oe-b9n)R(9M&sN_q~*l5hvdcDInfO|Zo!cTzMO`efE zqQGjTq)xW!Zk5VEgw{Zp;Zc3rslv;FuZOV9)0w@~im{q_t#;z@@_wej;_mU+=j+bi zy>KxHFs?scc z5WbjVRB{prM_hP_5I=w&`5Q0^Y-?s z^7kpSu(z@jWKhy{DYdlp^RM)?D0T`H7GPlH*5mTE3 + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/Constants.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/Constants.java new file mode 100644 index 00000000..4a265f43 --- /dev/null +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/Constants.java @@ -0,0 +1,9 @@ +package com.microsoft.copilot.eclipse.ui; + +/** + * A class to hold all the public constants used in the GitHub Copilot UI. + */ +public class Constants { + public static final int TOOLBAR_ICON_WIDTH_IN_PIEXL = 16; + public static final int TOOLBAR_ICON_HEIGHT_IN_PIEXL = 16; +} diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/ShowStatusBarMenuHandler.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/ShowStatusBarMenuHandler.java new file mode 100644 index 00000000..1af47c32 --- /dev/null +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/ShowStatusBarMenuHandler.java @@ -0,0 +1,55 @@ +package com.microsoft.copilot.eclipse.ui.handlers; + +import org.eclipse.core.commands.AbstractHandler; +import org.eclipse.core.commands.ExecutionEvent; +import org.eclipse.core.commands.ExecutionException; +import org.eclipse.jface.action.Action; +import org.eclipse.jface.action.MenuManager; +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.swt.widgets.Menu; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.ui.PlatformUI; + +import com.microsoft.copilot.eclipse.ui.Constants; +import com.microsoft.copilot.eclipse.ui.utils.UiUtils; + +/** + * Handler for showing GitHub Copilot status bar menu. + */ +public class ShowStatusBarMenuHandler extends AbstractHandler { + + /** + * Render the status bar menu based on the logged-in state. + */ + @Override + public Object execute(ExecutionEvent event) throws ExecutionException { + + Shell shell = PlatformUI.getWorkbench().getDisplay().getActiveShell(); + MenuManager menuManager = new MenuManager(); + ImageDescriptor icon = UiUtils.resizeIcon("/icons/copilot.png", Constants.TOOLBAR_ICON_WIDTH_IN_PIEXL, + Constants.TOOLBAR_ICON_HEIGHT_IN_PIEXL); + + // TODO: Add GitHub sign-in states to the menu + Action signInAction = new Action("Sign In to GitHub", icon) { + @Override + public void run() { + // Handle sign-in action + } + }; + + // TODO: Add GitHub sign-out states to the menu + Action signOutAction = new Action("Sign Out from GitHub", icon) { + @Override + public void run() { + // Handle sign-out action + } + }; + + menuManager.add(signInAction); + menuManager.add(signOutAction); + + Menu menu = menuManager.createContextMenu(shell); + menu.setVisible(true); + return null; + } +} diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/utils/UiUtils.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/utils/UiUtils.java index 2f609300..992eefe8 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/utils/UiUtils.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/utils/UiUtils.java @@ -4,6 +4,11 @@ import org.eclipse.core.resources.IFile; import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.ImageData; +import org.eclipse.swt.graphics.ImageLoader; +import org.eclipse.swt.widgets.Display; import org.eclipse.ui.IEditorInput; import org.eclipse.ui.IFileEditorInput; import org.eclipse.ui.texteditor.ITextEditor; @@ -31,4 +36,20 @@ public static URI getUriFromTextEditor(ITextEditor editor) { return null; } + /** + * Resizes the icon at the given path to the given width and height. + * Icon size is 16x16 by default, which is the recommended size for toolbar icons. + * For more details: https://eclipse-platform.github.io/ui-best-practices/#toolbar + */ + public static ImageDescriptor resizeIcon(String path, int width, int height) { + ImageLoader loader = new ImageLoader(); + ImageData[] imageDataArray = loader.load(UiUtils.class.getResourceAsStream(path)); + if (imageDataArray.length > 0) { + ImageData imageData = imageDataArray[0].scaledTo(width, height); + Image image = new Image(Display.getDefault(), imageData); + return ImageDescriptor.createFromImage(image); + } + return null; + } + } From 7333c43437e9f1341405d87cbe45ef829b642e9f Mon Sep 17 00:00:00 2001 From: Sheng Chen Date: Wed, 11 Dec 2024 15:07:27 +0800 Subject: [PATCH 017/690] feat - Trigger inline completion on document change (#23) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../copilot/eclipse/core/Constants.java | 13 ++ .../core/lsp/CopilotLanguageServer.java | 8 ++ .../lsp/CopilotLanguageServerConnection.java | 13 +- .../core/lsp/protocol/CompletionDocument.java | 106 +++++++++++++++ .../core/lsp/protocol/CompletionItem.java | 127 ++++++++++++++++++ .../core/lsp/protocol/CompletionParams.java | 70 ++++++++++ .../core/lsp/protocol/CompletionResult.java | 58 ++++++++ .../ui/completion/CompletionJobTests.java | 72 ++++++++++ .../META-INF/MANIFEST.MF | 3 +- .../ui/{Constants.java => UiConstants.java} | 7 +- .../ui/completion/CompletionHandler.java | 88 +++++++++++- .../eclipse/ui/completion/CompletionJob.java | 66 +++++++++ .../ui/handlers/ShowStatusBarMenuHandler.java | 6 +- .../copilot/eclipse/ui/utils/UiUtils.java | 21 ++- 14 files changed, 646 insertions(+), 12 deletions(-) create mode 100644 com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/Constants.java create mode 100644 com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/CompletionDocument.java create mode 100644 com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/CompletionItem.java create mode 100644 com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/CompletionParams.java create mode 100644 com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/CompletionResult.java create mode 100644 com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/completion/CompletionJobTests.java rename com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/{Constants.java => UiConstants.java} (73%) create mode 100644 com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionJob.java diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/Constants.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/Constants.java new file mode 100644 index 00000000..9894b8e0 --- /dev/null +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/Constants.java @@ -0,0 +1,13 @@ +package com.microsoft.copilot.eclipse.core; + +/** + * A class to hold all the public constants used in the GitHub Copilot core. + */ +public class Constants { + + private Constants() { + // prevent instantiation + } + + public static final String PLUGIN_ID = "com.microsoft.copilot.eclipse"; +} diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/CopilotLanguageServer.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/CopilotLanguageServer.java index 1945d79f..8b6e8e45 100644 --- a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/CopilotLanguageServer.java +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/CopilotLanguageServer.java @@ -7,6 +7,8 @@ import com.microsoft.copilot.eclipse.core.lsp.protocol.AuthStatusResult; import com.microsoft.copilot.eclipse.core.lsp.protocol.CheckStatusParams; +import com.microsoft.copilot.eclipse.core.lsp.protocol.CompletionParams; +import com.microsoft.copilot.eclipse.core.lsp.protocol.CompletionResult; /** * Interface for Copilot Language Server. @@ -19,4 +21,10 @@ public interface CopilotLanguageServer extends LanguageServer { @JsonRequest CompletableFuture checkStatus(CheckStatusParams param); + /** + * Get single completion for the given parameters. + */ + @JsonRequest + CompletableFuture getCompletions(CompletionParams params); + } diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/CopilotLanguageServerConnection.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/CopilotLanguageServerConnection.java index 0268b230..ac1d23b1 100644 --- a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/CopilotLanguageServerConnection.java +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/CopilotLanguageServerConnection.java @@ -11,11 +11,13 @@ import com.microsoft.copilot.eclipse.core.lsp.protocol.AuthStatusResult; import com.microsoft.copilot.eclipse.core.lsp.protocol.CheckStatusParams; +import com.microsoft.copilot.eclipse.core.lsp.protocol.CompletionParams; +import com.microsoft.copilot.eclipse.core.lsp.protocol.CompletionResult; /** * Language Server for Copilot agent. */ -@SuppressWarnings({ "restriction", "null" }) +@SuppressWarnings({ "restriction" }) public class CopilotLanguageServerConnection { public static final String SERVER_ID = "com.microsoft.copilot.eclipse.ls"; @@ -65,6 +67,15 @@ public CompletableFuture checkStatus(Boolean localCheckOnly) { return this.languageServerWrapper.execute(fn); } + /** + * Get single completion for the given parameters. + */ + public CompletableFuture getCompletions(CompletionParams params) { + Function> fn = server -> ((CopilotLanguageServer) server) + .getCompletions(params); + return this.languageServerWrapper.execute(fn); + } + /** * Stop the language server. */ diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/CompletionDocument.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/CompletionDocument.java new file mode 100644 index 00000000..2e718667 --- /dev/null +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/CompletionDocument.java @@ -0,0 +1,106 @@ +package com.microsoft.copilot.eclipse.core.lsp.protocol; + +import java.util.Objects; + +import org.eclipse.lsp4j.Position; +import org.eclipse.lsp4j.jsonrpc.util.ToStringBuilder; +import org.eclipse.lsp4j.jsonrpc.validation.NonNull; + +/** + * Document information for completion. + */ +public class CompletionDocument { + + @NonNull + private String uri; + + @NonNull + private Position position; + + private boolean insertSpaces; + + private int tabSize; + + private int version; + + /** + * Create a new CompletionDocument. + */ + public CompletionDocument(@NonNull String uri, @NonNull Position position) { + this.uri = uri; + this.position = position; + } + + public String getUri() { + return uri; + } + + public void setUri(String uri) { + this.uri = uri; + } + + public Position getPosition() { + return position; + } + + public void setPosition(Position position) { + this.position = position; + } + + public boolean isInsertSpaces() { + return insertSpaces; + } + + public void setInsertSpaces(boolean insertSpaces) { + this.insertSpaces = insertSpaces; + } + + public int getTabSize() { + return tabSize; + } + + public void setTabSize(int tabSize) { + this.tabSize = tabSize; + } + + public int getVersion() { + return version; + } + + public void setVersion(int version) { + this.version = version; + } + + @Override + public int hashCode() { + return Objects.hash(insertSpaces, position, tabSize, uri, version); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + CompletionDocument other = (CompletionDocument) obj; + return insertSpaces == other.insertSpaces && Objects.equals(position, other.position) && tabSize == other.tabSize + && Objects.equals(uri, other.uri) && version == other.version; + } + + @Override + public String toString() { + ToStringBuilder builder = new ToStringBuilder(this); + builder.add("uri", uri); + builder.add("position", position); + builder.add("insertSpaces", insertSpaces); + builder.add("tabSize", tabSize); + builder.add("version", version); + return builder.toString(); + } + +} diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/CompletionItem.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/CompletionItem.java new file mode 100644 index 00000000..5d71cd2e --- /dev/null +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/CompletionItem.java @@ -0,0 +1,127 @@ +package com.microsoft.copilot.eclipse.core.lsp.protocol; + +import java.util.Objects; + +import org.eclipse.lsp4j.Position; +import org.eclipse.lsp4j.Range; +import org.eclipse.lsp4j.jsonrpc.util.ToStringBuilder; +import org.eclipse.lsp4j.jsonrpc.validation.NonNull; + +/** + * An item of a completion result list. + */ +public class CompletionItem { + + @NonNull + private String uuid; + + @NonNull + private String text; + + @NonNull + private Range range; + + @NonNull + private String displayText; + + @NonNull + Position position; + + @NonNull + private int docVersion; + + /** + * Creates a new CompletionItem. + */ + public CompletionItem(@NonNull String uuid, @NonNull String text, @NonNull Range range, @NonNull String displayText, + @NonNull Position position, @NonNull int docVersion) { + this.uuid = uuid; + this.text = text; + this.range = range; + this.displayText = displayText; + this.position = position; + this.docVersion = docVersion; + } + + public String getUuid() { + return uuid; + } + + public void setUuid(String uuid) { + this.uuid = uuid; + } + + public String getText() { + return text; + } + + public void setText(String text) { + this.text = text; + } + + public Range getRange() { + return range; + } + + public void setRange(Range range) { + this.range = range; + } + + public String getDisplayText() { + return displayText; + } + + public void setDisplayText(String displayText) { + this.displayText = displayText; + } + + public Position getPosition() { + return position; + } + + public void setPosition(Position position) { + this.position = position; + } + + public int getDocVersion() { + return docVersion; + } + + public void setDocVersion(int docVersion) { + this.docVersion = docVersion; + } + + @Override + public int hashCode() { + return Objects.hash(displayText, docVersion, position, range, text, uuid); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + CompletionItem other = (CompletionItem) obj; + return Objects.equals(displayText, other.displayText) && docVersion == other.docVersion + && Objects.equals(position, other.position) && Objects.equals(range, other.range) + && Objects.equals(text, other.text) && Objects.equals(uuid, other.uuid); + } + + @Override + public String toString() { + ToStringBuilder builder = new ToStringBuilder(this); + builder.add("uuid", uuid); + builder.add("text", text); + builder.add("range", range); + builder.add("displayText", displayText); + builder.add("position", position); + builder.add("docVersion", docVersion); + return builder.toString(); + } +} diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/CompletionParams.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/CompletionParams.java new file mode 100644 index 00000000..1884b4d1 --- /dev/null +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/CompletionParams.java @@ -0,0 +1,70 @@ +package com.microsoft.copilot.eclipse.core.lsp.protocol; + +import java.util.Map; +import java.util.Objects; + +import org.eclipse.lsp4j.jsonrpc.util.ToStringBuilder; +import org.eclipse.lsp4j.jsonrpc.validation.NonNull; + +/** + * Parameter for getCompletion request. + */ +public class CompletionParams { + + @NonNull + private CompletionDocument doc; + + private Map options; + + /** + * Create a new parameter for getCompletion request. + */ + public CompletionParams(@NonNull CompletionDocument doc) { + this.doc = doc; + } + + public CompletionDocument getDoc() { + return doc; + } + + public void setDoc(CompletionDocument doc) { + this.doc = doc; + } + + public Map getOptions() { + return options; + } + + public void setOptions(Map options) { + this.options = options; + } + + @Override + public int hashCode() { + return Objects.hash(doc, options); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + CompletionParams other = (CompletionParams) obj; + return Objects.equals(doc, other.doc) && Objects.equals(options, other.options); + } + + @Override + public String toString() { + ToStringBuilder builder = new ToStringBuilder(this); + builder.add("doc", doc); + builder.add("options", options); + return builder.toString(); + } + +} diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/CompletionResult.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/CompletionResult.java new file mode 100644 index 00000000..f321f0d5 --- /dev/null +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/CompletionResult.java @@ -0,0 +1,58 @@ +package com.microsoft.copilot.eclipse.core.lsp.protocol; + +import java.util.List; +import java.util.Objects; + +import org.eclipse.lsp4j.jsonrpc.util.ToStringBuilder; +import org.eclipse.lsp4j.jsonrpc.validation.NonNull; + +/** + * Result for getCompletions & getCompletionsCycling. + */ +public class CompletionResult { + + @NonNull + private List completions; + + /** + * Creates a new CompletionResult. + */ + public CompletionResult(@NonNull List completions) { + this.completions = completions; + } + + public List getCompletions() { + return completions; + } + + public void setCompletions(List completions) { + this.completions = completions; + } + + @Override + public int hashCode() { + return Objects.hash(completions); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + CompletionResult other = (CompletionResult) obj; + return Objects.equals(completions, other.completions); + } + + @Override + public String toString() { + ToStringBuilder builder = new ToStringBuilder(this); + builder.add("completions", completions); + return builder.toString(); + } +} diff --git a/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/completion/CompletionJobTests.java b/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/completion/CompletionJobTests.java new file mode 100644 index 00000000..5c0f848c --- /dev/null +++ b/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/completion/CompletionJobTests.java @@ -0,0 +1,72 @@ +package com.microsoft.copilot.eclipse.ui.completion; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +import java.util.concurrent.CompletableFuture; + +import org.eclipse.core.runtime.IStatus; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; + +import com.microsoft.copilot.eclipse.core.lsp.CopilotLanguageServerConnection; +import com.microsoft.copilot.eclipse.core.lsp.protocol.CompletionDocument; +import com.microsoft.copilot.eclipse.core.lsp.protocol.CompletionParams; +import com.microsoft.copilot.eclipse.core.lsp.protocol.CompletionResult; + +@ExtendWith(MockitoExtension.class) +class CompletionJobTests { + + private CopilotLanguageServerConnection mockLsConnection; + private CompletionJob completionJob; + + @BeforeEach + public void setUp() { + mockLsConnection = mock(CopilotLanguageServerConnection.class); + completionJob = new CompletionJob(mockLsConnection); + } + + @Test + void testTriggerCompletionJobWithoutParams() throws InterruptedException { + completionJob.schedule(); + completionJob.join(); + + IStatus status = completionJob.getResult(); + + assertEquals(IStatus.ERROR, status.getSeverity()); + } + + @Test + void testCancelCompletionJob() throws InterruptedException { + completionJob.schedule(500L); + completionJob.cancel(); + completionJob.join(); + + IStatus status = completionJob.getResult(); + + if (status != null) { + assertEquals(IStatus.CANCEL, status.getSeverity()); + } + } + + @Test + void testTriggerCompletionJobWithParams() throws InterruptedException { + CompletionDocument document = mock(CompletionDocument.class); + CompletionParams params = new CompletionParams(document); + completionJob.setCompletionParams(params); + + CompletionResult expectedResult = new CompletionResult(new ArrayList<>()); + CompletableFuture future = CompletableFuture.completedFuture(expectedResult); + when(mockLsConnection.getCompletions(params)).thenReturn(future); + completionJob.schedule(); + completionJob.join(); + + IStatus status = completionJob.getResult(); + assertEquals(IStatus.OK, status.getSeverity()); + assertEquals(expectedResult, completionJob.getCompletionResult()); + } +} diff --git a/com.microsoft.copilot.eclipse.ui/META-INF/MANIFEST.MF b/com.microsoft.copilot.eclipse.ui/META-INF/MANIFEST.MF index b1a4e699..ed31467e 100644 --- a/com.microsoft.copilot.eclipse.ui/META-INF/MANIFEST.MF +++ b/com.microsoft.copilot.eclipse.ui/META-INF/MANIFEST.MF @@ -18,4 +18,5 @@ Require-Bundle: com.microsoft.copilot.eclipse.core;bundle-version="0.1.0", org.eclipse.core.runtime;bundle-version="3.31.100", org.eclipse.jdt.annotation, org.eclipse.core.resources, - org.eclipse.lsp4e + org.eclipse.lsp4e, + org.eclipse.lsp4j diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/Constants.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/UiConstants.java similarity index 73% rename from com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/Constants.java rename to com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/UiConstants.java index 4a265f43..8c54fac5 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/Constants.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/UiConstants.java @@ -3,7 +3,12 @@ /** * A class to hold all the public constants used in the GitHub Copilot UI. */ -public class Constants { +public class UiConstants { + + private UiConstants() { + // prevent instantiation + } + public static final int TOOLBAR_ICON_WIDTH_IN_PIEXL = 16; public static final int TOOLBAR_ICON_HEIGHT_IN_PIEXL = 16; } diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionHandler.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionHandler.java index 7ef0ace3..ec95185f 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionHandler.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionHandler.java @@ -3,37 +3,49 @@ import java.io.IOException; import java.net.URI; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.jobs.IJobChangeEvent; +import org.eclipse.core.runtime.jobs.IJobChangeListener; +import org.eclipse.jface.text.BadLocationException; import org.eclipse.jface.text.IDocument; import org.eclipse.jface.text.ITextListener; import org.eclipse.jface.text.ITextOperationTarget; import org.eclipse.jface.text.ITextViewer; import org.eclipse.jface.text.TextEvent; import org.eclipse.lsp4e.LSPEclipseUtils; +import org.eclipse.lsp4j.Position; import org.eclipse.swt.custom.CaretEvent; import org.eclipse.swt.custom.CaretListener; import org.eclipse.ui.texteditor.ITextEditor; import com.microsoft.copilot.eclipse.core.lsp.CopilotLanguageServerConnection; +import com.microsoft.copilot.eclipse.core.lsp.protocol.CompletionDocument; +import com.microsoft.copilot.eclipse.core.lsp.protocol.CompletionParams; +import com.microsoft.copilot.eclipse.core.lsp.protocol.CompletionResult; import com.microsoft.copilot.eclipse.ui.utils.SwtUtils; import com.microsoft.copilot.eclipse.ui.utils.UiUtils; /** * Handle completion for an ITextEditor. */ -public class CompletionHandler implements ITextListener, CaretListener { +public class CompletionHandler implements ITextListener, CaretListener, IJobChangeListener { private CopilotLanguageServerConnection lsConnection; private ITextEditor editor; private ITextViewer textViewer; private IDocument document; private URI documentUri; - private int documentVersion; + private volatile int documentVersion; + + private CompletionJob completionJob; /** * Creates a new completion handler. */ public CompletionHandler(CopilotLanguageServerConnection lsConnection, ITextEditor editor) { this.lsConnection = lsConnection; + this.completionJob = new CompletionJob(lsConnection); + this.completionJob.addJobChangeListener(this); this.editor = editor; this.textViewer = (ITextViewer) this.editor.getAdapter(ITextOperationTarget.class); this.document = LSPEclipseUtils.getDocument(editor); @@ -58,7 +70,7 @@ public void caretMoved(CaretEvent event) { // TODO: remove ghost text } else { this.documentVersion = currentVersion; - // TODO: trigger completion + triggerCompletion(); } } @@ -75,6 +87,8 @@ public void textChanged(TextEvent event) { * Disposes the resources of this completion handler. */ public void dispose() { + this.completionJob.cancel(); + this.completionJob.removeJobChangeListener(this); lsConnection.disconnectDocument(this.documentUri); if (this.textViewer != null) { SwtUtils.invokeOnDisplayThread(() -> { @@ -99,4 +113,72 @@ void registerListeners() { }); } + void triggerCompletion() { + CompletionParams completionParam = null; + try { + completionParam = this.createCompletionParams(UiUtils.getCaretOffset(this.editor)); + } catch (BadLocationException e) { + // TODO: log & send telemetry + return; + } + this.completionJob.cancel(); + this.completionJob.setCompletionParams(completionParam); + this.completionJob.schedule(); + } + + CompletionParams createCompletionParams(int offset) throws BadLocationException { + String uriString = this.documentUri.toASCIIString(); + Position position = LSPEclipseUtils.toPosition(offset, this.document); + CompletionDocument completionDoc = new CompletionDocument(uriString, position); + completionDoc.setVersion(this.documentVersion); + // TODO: following format options are hard-coded, need to revisit later when + // implementing the multi-line completion + completionDoc.setInsertSpaces(true); + completionDoc.setTabSize(4); + return new CompletionParams(completionDoc); + } + + @Override + public void aboutToRun(IJobChangeEvent event) { + // do nothing + } + + @Override + public void awake(IJobChangeEvent event) { + // do nothing + } + + @Override + public void done(IJobChangeEvent event) { + IStatus jobStatus = this.completionJob.getResult(); + if (jobStatus != null && !jobStatus.isOK()) { + return; + // TODO: log & send telemetry + } + CompletionResult result = this.completionJob.getCompletionResult(); + if (result == null || result.getCompletions() == null || result.getCompletions().isEmpty()) { + return; + } + // ignore the result if the document version is out-dated. + if (this.documentVersion != result.getCompletions().get(0).getDocVersion()) { + return; + } + // TODO: render completion items + } + + @Override + public void running(IJobChangeEvent event) { + // do nothing + } + + @Override + public void scheduled(IJobChangeEvent event) { + // do nothing + } + + @Override + public void sleeping(IJobChangeEvent event) { + // do nothing + } + } diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionJob.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionJob.java new file mode 100644 index 00000000..a38bd213 --- /dev/null +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionJob.java @@ -0,0 +1,66 @@ +package com.microsoft.copilot.eclipse.ui.completion; + +import java.util.concurrent.ExecutionException; + +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Status; +import org.eclipse.core.runtime.jobs.Job; + +import com.microsoft.copilot.eclipse.core.Constants; +import com.microsoft.copilot.eclipse.core.lsp.CopilotLanguageServerConnection; +import com.microsoft.copilot.eclipse.core.lsp.protocol.CompletionParams; +import com.microsoft.copilot.eclipse.core.lsp.protocol.CompletionResult; + +/** + * Job to trigger an inline completion. + */ +public class CompletionJob extends Job { + + private CompletionResult result; + + /** + * Creates a new completion job. + */ + public CompletionJob(CopilotLanguageServerConnection lsConnection) { + super("Generating completion..."); + this.lsConnection = lsConnection; + this.setUser(true); + } + + private CopilotLanguageServerConnection lsConnection; + private CompletionParams params; + + public void setCompletionParams(CompletionParams params) { + this.params = params; + } + + @Override + protected IStatus run(IProgressMonitor monitor) { + if (params == null) { + return new Status(IStatus.ERROR, Constants.PLUGIN_ID, "Invalid completion parameters"); + } + if (monitor.isCanceled()) { + System.out.println("Completion job is canceled"); + return Status.CANCEL_STATUS; + } + try { + this.result = this.lsConnection.getCompletions(params).get(); + } catch (InterruptedException e) { + return Status.CANCEL_STATUS; + } catch (ExecutionException e) { + // TODO: log & send telemetry + return new Status(IStatus.ERROR, Constants.PLUGIN_ID, e.getMessage(), e); + } + if (monitor.isCanceled()) { + System.out.println("Completion job is canceled"); + return Status.CANCEL_STATUS; + } + return Status.OK_STATUS; + } + + public CompletionResult getCompletionResult() { + return result; + } + +} diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/ShowStatusBarMenuHandler.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/ShowStatusBarMenuHandler.java index 1af47c32..17f4ef64 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/ShowStatusBarMenuHandler.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/ShowStatusBarMenuHandler.java @@ -10,7 +10,7 @@ import org.eclipse.swt.widgets.Shell; import org.eclipse.ui.PlatformUI; -import com.microsoft.copilot.eclipse.ui.Constants; +import com.microsoft.copilot.eclipse.ui.UiConstants; import com.microsoft.copilot.eclipse.ui.utils.UiUtils; /** @@ -26,8 +26,8 @@ public Object execute(ExecutionEvent event) throws ExecutionException { Shell shell = PlatformUI.getWorkbench().getDisplay().getActiveShell(); MenuManager menuManager = new MenuManager(); - ImageDescriptor icon = UiUtils.resizeIcon("/icons/copilot.png", Constants.TOOLBAR_ICON_WIDTH_IN_PIEXL, - Constants.TOOLBAR_ICON_HEIGHT_IN_PIEXL); + ImageDescriptor icon = UiUtils.resizeIcon("/icons/copilot.png", UiConstants.TOOLBAR_ICON_WIDTH_IN_PIEXL, + UiConstants.TOOLBAR_ICON_HEIGHT_IN_PIEXL); // TODO: Add GitHub sign-in states to the menu Action signInAction = new Action("Sign In to GitHub", icon) { diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/utils/UiUtils.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/utils/UiUtils.java index 992eefe8..95d0e2fa 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/utils/UiUtils.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/utils/UiUtils.java @@ -1,13 +1,16 @@ package com.microsoft.copilot.eclipse.ui.utils; import java.net.URI; +import java.util.concurrent.atomic.AtomicInteger; import org.eclipse.core.resources.IFile; import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.swt.custom.StyledText; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.graphics.ImageData; import org.eclipse.swt.graphics.ImageLoader; +import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Display; import org.eclipse.ui.IEditorInput; import org.eclipse.ui.IFileEditorInput; @@ -37,9 +40,8 @@ public static URI getUriFromTextEditor(ITextEditor editor) { } /** - * Resizes the icon at the given path to the given width and height. - * Icon size is 16x16 by default, which is the recommended size for toolbar icons. - * For more details: https://eclipse-platform.github.io/ui-best-practices/#toolbar + * Resizes the icon at the given path to the given width and height. Icon size is 16x16 by default, which is the + * recommended size for toolbar icons. For more details: https://eclipse-platform.github.io/ui-best-practices/#toolbar */ public static ImageDescriptor resizeIcon(String path, int width, int height) { ImageLoader loader = new ImageLoader(); @@ -52,4 +54,17 @@ public static ImageDescriptor resizeIcon(String path, int width, int height) { return null; } + /** + * Gets the caret offset of the given text editor. + */ + public static int getCaretOffset(ITextEditor editor) { + final AtomicInteger ref = new AtomicInteger(0); + StyledText styledText = (StyledText) editor.getAdapter(Control.class); + SwtUtils.invokeOnDisplayThread(() -> { + int offset = styledText.getCaretOffset(); + ref.set(offset); + }, styledText); + return ref.get(); + } + } From c3250360f536e249d945f41b8fffcc5464976f6d Mon Sep 17 00:00:00 2001 From: ethanyhou <149548697+ethanyhou@users.noreply.github.com> Date: Thu, 12 Dec 2024 13:16:55 +0800 Subject: [PATCH 018/690] refactor - Extracts plain message for the i18n. (#24) --- .../copilot/eclipse/ui/i18n/MessagesTest.java | 15 ++++++++++++++ .../META-INF/MANIFEST.MF | 3 ++- .../ui/handlers/ShowStatusBarMenuHandler.java | 5 +++-- .../copilot/eclipse/ui/i18n/Messages.java | 20 +++++++++++++++++++ .../eclipse/ui/i18n/messages.properties | 2 ++ 5 files changed, 42 insertions(+), 3 deletions(-) create mode 100644 com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/i18n/MessagesTest.java create mode 100644 com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/i18n/Messages.java create mode 100644 com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/i18n/messages.properties diff --git a/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/i18n/MessagesTest.java b/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/i18n/MessagesTest.java new file mode 100644 index 00000000..38a9e62b --- /dev/null +++ b/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/i18n/MessagesTest.java @@ -0,0 +1,15 @@ +package com.microsoft.copilot.eclipse.ui.i18n; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import org.junit.jupiter.api.Test; + +class MessagesTest { + + @Test + void testMessagesInitialization() { + // Ensure that the static fields are initialized + assertNotNull(Messages.INFO_signToGitHub); + assertNotNull(Messages.INFO_signOutFromGitHub); + } +} \ No newline at end of file diff --git a/com.microsoft.copilot.eclipse.ui/META-INF/MANIFEST.MF b/com.microsoft.copilot.eclipse.ui/META-INF/MANIFEST.MF index ed31467e..c3778859 100644 --- a/com.microsoft.copilot.eclipse.ui/META-INF/MANIFEST.MF +++ b/com.microsoft.copilot.eclipse.ui/META-INF/MANIFEST.MF @@ -4,7 +4,8 @@ Bundle-Name: com.microsoft.copilot.eclipse.ui Bundle-SymbolicName: com.microsoft.copilot.eclipse.ui;singleton:=true Bundle-Version: 0.1.0.qualifier Export-Package: com.microsoft.copilot.eclipse.ui, - com.microsoft.copilot.eclipse.ui.completion + com.microsoft.copilot.eclipse.ui.completion, + com.microsoft.copilot.eclipse.ui.i18n Bundle-Activator: com.microsoft.copilot.eclipse.ui.CopilotUi Bundle-RequiredExecutionEnvironment: JavaSE-17 Automatic-Module-Name: com.microsoft.copilot.eclipse.ui diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/ShowStatusBarMenuHandler.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/ShowStatusBarMenuHandler.java index 17f4ef64..d94cdace 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/ShowStatusBarMenuHandler.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/ShowStatusBarMenuHandler.java @@ -11,6 +11,7 @@ import org.eclipse.ui.PlatformUI; import com.microsoft.copilot.eclipse.ui.UiConstants; +import com.microsoft.copilot.eclipse.ui.i18n.Messages; import com.microsoft.copilot.eclipse.ui.utils.UiUtils; /** @@ -30,7 +31,7 @@ public Object execute(ExecutionEvent event) throws ExecutionException { UiConstants.TOOLBAR_ICON_HEIGHT_IN_PIEXL); // TODO: Add GitHub sign-in states to the menu - Action signInAction = new Action("Sign In to GitHub", icon) { + Action signInAction = new Action(Messages.INFO_signToGitHub, icon) { @Override public void run() { // Handle sign-in action @@ -38,7 +39,7 @@ public void run() { }; // TODO: Add GitHub sign-out states to the menu - Action signOutAction = new Action("Sign Out from GitHub", icon) { + Action signOutAction = new Action(Messages.INFO_signOutFromGitHub, icon) { @Override public void run() { // Handle sign-out action diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/i18n/Messages.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/i18n/Messages.java new file mode 100644 index 00000000..94e0afaa --- /dev/null +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/i18n/Messages.java @@ -0,0 +1,20 @@ +package com.microsoft.copilot.eclipse.ui.i18n; + +import org.eclipse.osgi.util.NLS; + +/** + * Message class for the i18n. + */ +public final class Messages extends NLS { + private static final String BUNDLE_NAME = "com.microsoft.copilot.eclipse.ui.i18n.messages"; //$NON-NLS-1$ + public static String INFO_signToGitHub; + public static String INFO_signOutFromGitHub; + + static { + // initialize resource bundle + NLS.initializeMessages(BUNDLE_NAME, Messages.class); + } + + private Messages() { + } +} \ No newline at end of file diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/i18n/messages.properties b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/i18n/messages.properties new file mode 100644 index 00000000..ae3f5feb --- /dev/null +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/i18n/messages.properties @@ -0,0 +1,2 @@ +INFO_signToGitHub=Sign In to GitHub +INFO_signOutFromGitHub=Sign Out from GitHub From 6d0ff44914b600d4705847b26ffd99b251a73f4c Mon Sep 17 00:00:00 2001 From: Sheng Chen Date: Thu, 12 Dec 2024 16:18:42 +0800 Subject: [PATCH 019/690] feat - Draw ghost text for inline completion (#25) --- .settings/dict | 1 + .../META-INF/MANIFEST.MF | 3 +- .../ui/completion/CompletionDataTests.java | 60 +++++++++ .../META-INF/MANIFEST.MF | 3 +- .../copilot/eclipse/ui/UiConstants.java | 7 + .../eclipse/ui/completion/CompletionData.java | 121 ++++++++++++++++++ .../ui/completion/CompletionHandler.java | 115 ++++++++++------- .../ui/completion/CompletionRendering.java | 117 +++++++++++++++++ 8 files changed, 382 insertions(+), 45 deletions(-) create mode 100644 com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/completion/CompletionDataTests.java create mode 100644 com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionData.java create mode 100644 com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionRendering.java diff --git a/.settings/dict b/.settings/dict index a003f6fd..cf5b8fcc 100644 --- a/.settings/dict +++ b/.settings/dict @@ -2,3 +2,4 @@ copilot plugin telemetry lifecycle +inline diff --git a/com.microsoft.copilot.eclipse.ui.test/META-INF/MANIFEST.MF b/com.microsoft.copilot.eclipse.ui.test/META-INF/MANIFEST.MF index 15589da4..b17ef99d 100644 --- a/com.microsoft.copilot.eclipse.ui.test/META-INF/MANIFEST.MF +++ b/com.microsoft.copilot.eclipse.ui.test/META-INF/MANIFEST.MF @@ -18,4 +18,5 @@ Require-Bundle: org.mockito.mockito-core;bundle-version="5.14.2", org.eclipse.ui;bundle-version="3.206.100", org.eclipse.ui.ide, org.eclipse.ui.workbench.texteditor, - org.eclipse.jface.text;bundle-version="3.25.200" + org.eclipse.jface.text;bundle-version="3.25.200", + org.eclipse.lsp4j;bundle-version="0.23.1" diff --git a/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/completion/CompletionDataTests.java b/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/completion/CompletionDataTests.java new file mode 100644 index 00000000..c3312f47 --- /dev/null +++ b/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/completion/CompletionDataTests.java @@ -0,0 +1,60 @@ +package com.microsoft.copilot.eclipse.ui.completion; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.List; + +import org.junit.jupiter.api.Test; + +import com.microsoft.copilot.eclipse.core.lsp.protocol.CompletionItem; + +class CompletionDataTests { + + @Test + void testGetEmptyStringWhenNoItems() { + CompletionData data = new CompletionData(); + assertEquals("", data.getText()); + } + + @Test + void testGetFirstLineWithSingleLineItem() { + CompletionData data = new CompletionData(); + data.setItems(List.of(new CompletionItem("uuid", "first", null, "first", null, 0))); + assertEquals("first", data.getFirstLine()); + } + + @Test + void testGetRemainingLineWithSingleLineItem() { + CompletionData data = new CompletionData(); + data.setItems(List.of(new CompletionItem("uuid", "first", null, "first", null, 0))); + assertEquals("", data.getRemainingLines()); + } + + @Test + void testGetFirstLineWithMultilineItem() { + CompletionData data = new CompletionData(); + data.setItems(List.of(new CompletionItem("uuid", "first\nsecond\nthird", null, "first\nsecond\nthird", null, 0))); + assertEquals("first", data.getFirstLine()); + } + + @Test + void testGetRemainingLinesWithMultilineItem() { + CompletionData data = new CompletionData(); + data.setItems(List.of(new CompletionItem("uuid", "first\nsecond\nthird", null, "first\nsecond\nthird", null, 0))); + assertEquals("second\nthird", data.getRemainingLines()); + } + + @Test + void getNumberOfLinesWhenNoItems() { + CompletionData completionData = new CompletionData(); + assertEquals(1, completionData.getNumberOfLines()); + } + + @Test + void getNumberOfLinesWithMultilineItem() { + CompletionData data = new CompletionData(); + data.setItems(List.of(new CompletionItem("uuid", "first\nsecond\nthird", null, "first\nsecond\nthird", null, 0))); + assertEquals(3, data.getNumberOfLines()); + } + +} diff --git a/com.microsoft.copilot.eclipse.ui/META-INF/MANIFEST.MF b/com.microsoft.copilot.eclipse.ui/META-INF/MANIFEST.MF index c3778859..b02c80b6 100644 --- a/com.microsoft.copilot.eclipse.ui/META-INF/MANIFEST.MF +++ b/com.microsoft.copilot.eclipse.ui/META-INF/MANIFEST.MF @@ -20,4 +20,5 @@ Require-Bundle: com.microsoft.copilot.eclipse.core;bundle-version="0.1.0", org.eclipse.jdt.annotation, org.eclipse.core.resources, org.eclipse.lsp4e, - org.eclipse.lsp4j + org.eclipse.lsp4j, + org.apache.commons.lang3 diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/UiConstants.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/UiConstants.java index 8c54fac5..b30d9d79 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/UiConstants.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/UiConstants.java @@ -1,5 +1,7 @@ package com.microsoft.copilot.eclipse.ui; +import org.eclipse.swt.graphics.RGB; + /** * A class to hold all the public constants used in the GitHub Copilot UI. */ @@ -11,4 +13,9 @@ private UiConstants() { public static final int TOOLBAR_ICON_WIDTH_IN_PIEXL = 16; public static final int TOOLBAR_ICON_HEIGHT_IN_PIEXL = 16; + + /** + * Default color for ghost text. + */ + public static final RGB DEFAULT_GHOST_TEXT_COLOR = new RGB(112, 112, 112); } diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionData.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionData.java new file mode 100644 index 00000000..3b1e0558 --- /dev/null +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionData.java @@ -0,0 +1,121 @@ +package com.microsoft.copilot.eclipse.ui.completion; + +import java.util.List; + +import com.microsoft.copilot.eclipse.core.lsp.protocol.CompletionItem; + +/** + * A class to hold data for the inline completion. + */ +public class CompletionData { + + /** + * A constant for an empty completion item. This is used to indicate that the completion is not available. Thus when + * clear the ghost text, we can set it to the rendering. + */ + public static final CompletionItem EMPTY_ITEM = new CompletionItem("", "", null, "", null, -1); + + private List items; + private int index; + private int triggerOffset; + + /** + * Creates a new CompletionData. + */ + public CompletionData() { + this.index = 0; + this.triggerOffset = -1; + } + + /** + * Get text for the current active completion item. + */ + public String getText() { + CompletionItem item = getCurrentItem(); + if (item == null) { + return ""; + } + return item.getDisplayText(); + } + + /** + * Get the first line of the current active completion item. + */ + public String getFirstLine() { + String text = getText(); + if (text == null) { + return ""; + } + return text.split("\n")[0]; + } + + /** + * Get the remaining lines of the current active completion item. + */ + public String getRemainingLines() { + String text = getText(); + if (text == null) { + return ""; + } + int lineBreakIdx = text.indexOf("\n"); + if (lineBreakIdx < 0) { + return ""; + } + return text.substring(lineBreakIdx + 1); + } + + /** + * Get the number of items in the completion list. + */ + public int getSize() { + if (items == null) { + return 0; + } + return items.size(); + } + + /** + * Get the document version when the completion was triggered. + */ + public int getDocumentVersion() { + CompletionItem item = getCurrentItem(); + if (item == null) { + return -1; + } + return item.getDocVersion(); + } + + /** + * Set the completion items. + */ + public void setItems(List items) { + this.items = items; + this.index = 0; + } + + public int getTriggerOffset() { + return triggerOffset; + } + + public void setTriggerOffset(int offset) { + this.triggerOffset = offset; + } + + /** + * Get the number of lines for the active completion text. + */ + public int getNumberOfLines() { + return this.getText().split("\n").length; + } + + /** + * Get the current active completion item. + */ + CompletionItem getCurrentItem() { + if (items == null || items.isEmpty() || index >= items.size()) { + return null; + } + return items.get(index); + } + +} diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionHandler.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionHandler.java index ec95185f..aa178773 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionHandler.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionHandler.java @@ -2,6 +2,7 @@ import java.io.IOException; import java.net.URI; +import java.util.List; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.jobs.IJobChangeEvent; @@ -38,6 +39,8 @@ public class CompletionHandler implements ITextListener, CaretListener, IJobChan private volatile int documentVersion; private CompletionJob completionJob; + private CompletionData completionData; + private CompletionRendering completionRendering; /** * Creates a new completion handler. @@ -45,29 +48,91 @@ public class CompletionHandler implements ITextListener, CaretListener, IJobChan public CompletionHandler(CopilotLanguageServerConnection lsConnection, ITextEditor editor) { this.lsConnection = lsConnection; this.completionJob = new CompletionJob(lsConnection); + this.completionData = new CompletionData(); this.completionJob.addJobChangeListener(this); this.editor = editor; this.textViewer = (ITextViewer) this.editor.getAdapter(ITextOperationTarget.class); + if (textViewer == null) { + // TODO: log & send telemetry + return; + } + this.completionRendering = new CompletionRendering(this.textViewer, this.completionData); this.document = LSPEclipseUtils.getDocument(editor); this.documentUri = UiUtils.getUriFromTextEditor(editor); - this.documentVersion = 0; try { lsConnection.connectDocument(this.document); } catch (IOException e) { // TODO: log & send telemetry return; } + this.documentVersion = -1; registerListeners(); } + void registerListeners() { + // if the text viewer is null, we will not register listeners. + // the side effect is that the completion will not be triggered for this editor. + if (this.textViewer == null) { + // TODO: log & send telemetry + return; + } + + SwtUtils.invokeOnDisplayThread(() -> { + this.textViewer.getTextWidget().addCaretListener(this); + this.textViewer.addTextListener(this); + }); + } + + void triggerCompletion() { + int caretOffset = UiUtils.getCaretOffset(this.editor); + this.completionData.setTriggerOffset(caretOffset); + CompletionParams completionParam = null; + try { + completionParam = this.createCompletionParams(caretOffset); + } catch (BadLocationException e) { + // TODO: log & send telemetry + return; + } + this.completionJob.cancel(); + this.completionJob.setCompletionParams(completionParam); + this.completionJob.schedule(); + } + + CompletionParams createCompletionParams(int offset) throws BadLocationException { + String uriString = this.documentUri.toASCIIString(); + Position position = LSPEclipseUtils.toPosition(offset, this.document); + CompletionDocument completionDoc = new CompletionDocument(uriString, position); + completionDoc.setVersion(this.documentVersion); + // following format options are hard-coded, because eclipse support applying the format options + // automatically when drawing text into the editor, so don't need to set the actual values here. + completionDoc.setInsertSpaces(true); + completionDoc.setTabSize(4); + return new CompletionParams(completionDoc); + } + + void clearCompletion() { + if (this.completionData.getSize() > 0) { + // if the completion data is not empty, clear it and trigger a redraw. + this.completionData.setItems(List.of(CompletionData.EMPTY_ITEM)); + this.completionRendering.redraw(); + } + } + @Override public void caretMoved(CaretEvent event) { // it's guaranteed that the document change event comes earlier than caret // change event. See org.eclipse.swt.custom.StyledText#modifyContent() int currentVersion = this.lsConnection.getDocumentVersion(this.documentUri); + + // initialize the document version and return. This avoids the ghost text + // being rendered when user opens the editor and just clicks in it. + if (this.documentVersion < 0) { + this.documentVersion = currentVersion; + return; + } if (currentVersion == this.documentVersion) { // if the caret position is changed without document version change, we should remove the ghost text. - // TODO: remove ghost text + clearCompletion(); } else { this.documentVersion = currentVersion; triggerCompletion(); @@ -94,50 +159,12 @@ public void dispose() { SwtUtils.invokeOnDisplayThread(() -> { this.textViewer.getTextWidget().removeCaretListener(this); this.textViewer.removeTextListener(this); + this.completionRendering.dispose(); }); } } - void registerListeners() { - // if the text viewer is null, we will not register listeners. - // the side effect is that the completion will not be triggered for this editor. - if (this.textViewer == null) { - // TODO: log & send telemetry - return; - } - - SwtUtils.invokeOnDisplayThread(() -> { - this.textViewer.getTextWidget().addCaretListener(this); - this.textViewer.addTextListener(this); - }); - } - - void triggerCompletion() { - CompletionParams completionParam = null; - try { - completionParam = this.createCompletionParams(UiUtils.getCaretOffset(this.editor)); - } catch (BadLocationException e) { - // TODO: log & send telemetry - return; - } - this.completionJob.cancel(); - this.completionJob.setCompletionParams(completionParam); - this.completionJob.schedule(); - } - - CompletionParams createCompletionParams(int offset) throws BadLocationException { - String uriString = this.documentUri.toASCIIString(); - Position position = LSPEclipseUtils.toPosition(offset, this.document); - CompletionDocument completionDoc = new CompletionDocument(uriString, position); - completionDoc.setVersion(this.documentVersion); - // TODO: following format options are hard-coded, need to revisit later when - // implementing the multi-line completion - completionDoc.setInsertSpaces(true); - completionDoc.setTabSize(4); - return new CompletionParams(completionDoc); - } - @Override public void aboutToRun(IJobChangeEvent event) { // do nothing @@ -160,10 +187,12 @@ public void done(IJobChangeEvent event) { return; } // ignore the result if the document version is out-dated. - if (this.documentVersion != result.getCompletions().get(0).getDocVersion()) { + if (this.documentVersion != this.completionData.getDocumentVersion()) { return; } - // TODO: render completion items + + this.completionData.setItems(result.getCompletions()); + this.completionRendering.redraw(); } @Override diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionRendering.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionRendering.java new file mode 100644 index 00000000..864b9615 --- /dev/null +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionRendering.java @@ -0,0 +1,117 @@ +package com.microsoft.copilot.eclipse.ui.completion; + +import org.apache.commons.lang3.StringUtils; +import org.eclipse.jface.text.ITextViewer; +import org.eclipse.swt.custom.StyledText; +import org.eclipse.swt.events.PaintEvent; +import org.eclipse.swt.events.PaintListener; +import org.eclipse.swt.graphics.Color; +import org.eclipse.swt.graphics.GC; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.swt.widgets.Display; + +import com.microsoft.copilot.eclipse.ui.UiConstants; +import com.microsoft.copilot.eclipse.ui.utils.SwtUtils; + +/** + * Render the ghost text for a completion item. + */ +public class CompletionRendering implements PaintListener { + + private ITextViewer textViewer; + private CompletionData completionData; + private Color ghostTextColor; + + /** + * Creates a new CompletionRendering. + */ + public CompletionRendering(ITextViewer textViewer, CompletionData completionData) { + this.completionData = completionData; + this.textViewer = textViewer; + StyledText styledText = textViewer.getTextWidget(); + if (styledText != null) { + SwtUtils.invokeOnDisplayThread(() -> { + styledText.addPaintListener(this); + this.ghostTextColor = new Color(Display.getCurrent(), UiConstants.DEFAULT_GHOST_TEXT_COLOR); + }); + } + + } + + public void setCompletionData(CompletionData completionData) { + this.completionData = completionData; + } + + @Override + public void paintControl(PaintEvent e) { + if (this.completionData == null || this.completionData.getTriggerOffset() < 0) { + return; + } + + StyledText styledText = textViewer.getTextWidget(); + if (styledText == null) { + return; + } + + GC gc = e.gc; + gc.setForeground(this.ghostTextColor); + // will get index out of bounds if the cursor is at the end. + // Because there is no more text to get bounds at EOF. + int caretOffset = Math.min(this.completionData.getTriggerOffset(), styledText.getCharCount() - 1); + String displayText = this.completionData.getText(); + + // set line vertical indentation + setLineVerticalIndentation(styledText, gc, caretOffset, displayText); + + String firstLine = this.completionData.getFirstLine(); + if (StringUtils.isNotBlank(firstLine)) { + Rectangle bounds = styledText.getTextBounds(caretOffset, caretOffset); + int y = bounds.y; + y += bounds.height - styledText.getLineHeight(); + gc.drawString(firstLine, bounds.x + bounds.width, y, true); + } + String remainingLines = this.completionData.getRemainingLines(); + if (StringUtils.isNotBlank(remainingLines)) { + int lineHt = styledText.getLineHeight(); + int fontHt = gc.getFontMetrics().getHeight(); + int x = styledText.getLeftMargin(); + Point offsetLocation = styledText.getLocationAtOffset(caretOffset); + int y = offsetLocation.y + lineHt * 2 - fontHt; + gc.drawText(remainingLines, x, y, true); + } + } + + /** + * Trigger a redraw event to update the ghost text. + */ + public void redraw() { + StyledText styledText = textViewer.getTextWidget(); + if (styledText != null) { + // TODO: can we use redrawRange() to improve the perf? + SwtUtils.invokeOnDisplayThread(() -> styledText.redraw()); + } + } + + /** + * Dispose the resources used by the rendering. + */ + public void dispose() { + if (this.ghostTextColor != null) { + this.ghostTextColor.dispose(); + this.ghostTextColor = null; + } + } + + private void setLineVerticalIndentation(StyledText styledText, GC gc, int caretOffset, String displayText) { + Point ghostTextExtent = gc.textExtent(displayText); + int numberOfLines = this.completionData.getNumberOfLines(); + if (numberOfLines <= 1) { + return; + } + int height = ghostTextExtent.y - ghostTextExtent.y / numberOfLines; + int lineIndex = styledText.getLineAtOffset(caretOffset); + styledText.setLineVerticalIndent(lineIndex + 1, height); + } + +} From 0494473ff15348a894d04e14b4c88c3e0a9eb4e9 Mon Sep 17 00:00:00 2001 From: Sheng Chen Date: Tue, 17 Dec 2024 10:05:51 +0800 Subject: [PATCH 020/690] feat - Support accept full suggestion (#27) This PR enables accepting full suggestions via a command. Meanwhile, it refactors the code logic about the completion rendering. * CompletionProvider - a singleton that used to provide the completion items. It holds a CompletionJob that talks to LS. * CompletionHandler - Class that listens to the completion related events from the editor(cursor move, document change...), and trigger the methods from CompletionManager accordingly. * CompletionManager - Class managing the completion, including calling the CompletionProvider for items, rendering/clearing the ghost text. --- .../META-INF/MANIFEST.MF | 5 +- .../completion/CompletionCollectionTests.java | 63 ++++++ .../completion/CompletionProviderTests.java | 49 +++++ .../META-INF/MANIFEST.MF | 1 + .../copilot/eclipse/core/CopilotCore.java | 7 + .../core/completion/CompletionCollection.java | 58 ++--- .../core}/completion/CompletionJob.java | 29 ++- .../core/completion/CompletionListener.java | 13 ++ .../core/completion/CompletionProvider.java | 123 +++++++++++ .../ui/completion/CompletionDataTests.java | 60 ------ .../ui/completion/CompletionJobTests.java | 1 + .../ui/completion/EditorManagerTests.java | 33 ++- .../AcceptFullSuggestionHandlerTests.java | 41 ++++ com.microsoft.copilot.eclipse.ui/.project | 4 +- .../META-INF/MANIFEST.MF | 1 + .../plugin.properties | 1 + com.microsoft.copilot.eclipse.ui/plugin.xml | 119 ++++++----- .../copilot/eclipse/ui/CopilotUi.java | 35 ++- .../copilot/eclipse/ui/UiConstants.java | 7 +- .../ui/completion/CompletionHandler.java | 189 +++++++---------- .../ui/completion/CompletionManager.java | 200 ++++++++++++++++++ .../ui/completion/CompletionRendering.java | 117 ---------- .../completion/EditorLifecycleListener.java | 9 +- .../eclipse/ui/completion/EditorsManager.java | 36 +++- .../handlers/AcceptFullSuggestionHandler.java | 30 +++ .../eclipse/ui/handlers/CopilotHandler.java | 29 +++ .../copilot/eclipse/ui/utils/UiUtils.java | 28 +-- 27 files changed, 860 insertions(+), 428 deletions(-) create mode 100644 com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/completion/CompletionCollectionTests.java create mode 100644 com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/completion/CompletionProviderTests.java rename com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionData.java => com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/completion/CompletionCollection.java (58%) rename {com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui => com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core}/completion/CompletionJob.java (77%) create mode 100644 com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/completion/CompletionListener.java create mode 100644 com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/completion/CompletionProvider.java delete mode 100644 com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/completion/CompletionDataTests.java create mode 100644 com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/handler/AcceptFullSuggestionHandlerTests.java create mode 100644 com.microsoft.copilot.eclipse.ui/plugin.properties create mode 100644 com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionManager.java delete mode 100644 com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionRendering.java create mode 100644 com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/AcceptFullSuggestionHandler.java create mode 100644 com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/CopilotHandler.java diff --git a/com.microsoft.copilot.eclipse.core.test/META-INF/MANIFEST.MF b/com.microsoft.copilot.eclipse.core.test/META-INF/MANIFEST.MF index 2112db88..e5428be1 100644 --- a/com.microsoft.copilot.eclipse.core.test/META-INF/MANIFEST.MF +++ b/com.microsoft.copilot.eclipse.core.test/META-INF/MANIFEST.MF @@ -12,4 +12,7 @@ Require-Bundle: com.microsoft.copilot.eclipse.core;bundle-version="0.1.0", org.eclipse.lsp4e;bundle-version="0.18.12", org.eclipse.jdt.annotation;bundle-version="2.3.0", junit-jupiter-api;bundle-version="5.11.0", - org.mockito.junit-jupiter;bundle-version="5.14.2" + org.mockito.junit-jupiter;bundle-version="5.14.2", + org.eclipse.lsp4j, + org.eclipse.core.jobs, + org.eclipse.equinox.common diff --git a/com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/completion/CompletionCollectionTests.java b/com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/completion/CompletionCollectionTests.java new file mode 100644 index 00000000..92b99a01 --- /dev/null +++ b/com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/completion/CompletionCollectionTests.java @@ -0,0 +1,63 @@ +package com.microsoft.copilot.eclipse.core.completion; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.Collections; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import com.microsoft.copilot.eclipse.core.lsp.protocol.CompletionItem; + +class CompletionCollectionTests { + + @Test + void constructorThrowsExceptionWhenCompletionsIsNull() { + assertThrows(IllegalArgumentException.class, () -> new CompletionCollection(null, "uri")); + } + + @Test + void constructorThrowsExceptionWhenCompletionsIsEmpty() { + assertThrows(IllegalArgumentException.class, () -> new CompletionCollection(Collections.emptyList(), "uri")); + } + + @Test + void getRemainingLinesReturnsEmptyStringWhenTextHasNoLineBreaks() { + CompletionItem mockItem = mock(CompletionItem.class); + List completions = List.of(mockItem); + CompletionCollection collection = new CompletionCollection(completions, "uri"); + when(mockItem.getDisplayText()).thenReturn("single line text"); + assertEquals("", collection.getRemainingLines()); + } + + @Test + void getFirstLineReturnsCorrectStringWhenTextHasLineBreaks() { + CompletionItem mockItem = mock(CompletionItem.class); + List completions = List.of(mockItem); + CompletionCollection collection = new CompletionCollection(completions, "uri"); + when(mockItem.getDisplayText()).thenReturn("line1\nline2\nline3"); + assertEquals("line1", collection.getFirstLine()); + } + + @Test + void getRemainingLineReturnsCorrectStringWhenTextHasLineBreaks() { + CompletionItem mockItem = mock(CompletionItem.class); + List completions = List.of(mockItem); + CompletionCollection collection = new CompletionCollection(completions, "uri"); + when(mockItem.getDisplayText()).thenReturn("line1\nline2\nline3"); + assertEquals("line2\nline3", collection.getRemainingLines()); + } + + @Test + void getNumberOfLinesReturnsCorrectNumberOfLines() { + CompletionItem mockItem = mock(CompletionItem.class); + List completions = List.of(mockItem); + CompletionCollection collection = new CompletionCollection(completions, "uri"); + when(mockItem.getDisplayText()).thenReturn("line1\nline2\nline3"); + assertEquals(3, collection.getNumberOfLines()); + } + +} diff --git a/com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/completion/CompletionProviderTests.java b/com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/completion/CompletionProviderTests.java new file mode 100644 index 00000000..e1dadf29 --- /dev/null +++ b/com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/completion/CompletionProviderTests.java @@ -0,0 +1,49 @@ +package com.microsoft.copilot.eclipse.core.completion; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.List; +import java.util.concurrent.CompletableFuture; + +import org.eclipse.core.runtime.NullProgressMonitor; +import org.eclipse.core.runtime.OperationCanceledException; +import org.eclipse.core.runtime.jobs.IJobManager; +import org.eclipse.core.runtime.jobs.Job; +import org.eclipse.lsp4j.Position; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import com.microsoft.copilot.eclipse.core.lsp.CopilotLanguageServerConnection; +import com.microsoft.copilot.eclipse.core.lsp.protocol.CompletionItem; +import com.microsoft.copilot.eclipse.core.lsp.protocol.CompletionResult; + +@ExtendWith(MockitoExtension.class) +class CompletionProviderTests { + + @Mock + private CopilotLanguageServerConnection mockLsConnection; + + @Mock + private CompletionListener mockListener; + + @Test + void testShouldNotifyListenersOnCompletion() throws OperationCanceledException, InterruptedException { + when(mockLsConnection.getCompletions(any())) + .thenReturn(CompletableFuture.completedFuture(new CompletionResult(List.of(mock(CompletionItem.class))))); + + CompletionProvider completionProvider = new CompletionProvider(mockLsConnection); + completionProvider.addCompletionListener(mockListener); + Position position = new Position(0, 0); + completionProvider.triggerCompletion("file://test.java", position, 1); + IJobManager jobManager = Job.getJobManager(); + jobManager.join(CompletionJob.COMPLETION_JOB_FAMILY, new NullProgressMonitor()); + verify(mockLsConnection, times(1)).getCompletions(any()); + } + +} diff --git a/com.microsoft.copilot.eclipse.core/META-INF/MANIFEST.MF b/com.microsoft.copilot.eclipse.core/META-INF/MANIFEST.MF index ab0e2d6d..21c7a7ce 100644 --- a/com.microsoft.copilot.eclipse.core/META-INF/MANIFEST.MF +++ b/com.microsoft.copilot.eclipse.core/META-INF/MANIFEST.MF @@ -4,6 +4,7 @@ Bundle-Name: com.microsoft.copilot.eclipse.core Bundle-SymbolicName: com.microsoft.copilot.eclipse.core;singleton:=true Bundle-Version: 0.1.0.qualifier Export-Package: com.microsoft.copilot.eclipse.core, + com.microsoft.copilot.eclipse.core.completion, com.microsoft.copilot.eclipse.core.lsp, com.microsoft.copilot.eclipse.core.lsp.protocol Bundle-Activator: com.microsoft.copilot.eclipse.core.CopilotCore diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/CopilotCore.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/CopilotCore.java index dbdf1297..bbfbd6c0 100644 --- a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/CopilotCore.java +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/CopilotCore.java @@ -10,6 +10,7 @@ import org.eclipse.lsp4e.LanguageServiceAccessor; import org.osgi.framework.BundleContext; +import com.microsoft.copilot.eclipse.core.completion.CompletionProvider; import com.microsoft.copilot.eclipse.core.lsp.CopilotLanguageServerConnection; /** @@ -19,6 +20,7 @@ public class CopilotCore extends Plugin { private CopilotLanguageServerConnection copilotLanguageServer; private AuthStatusManager authStatusManager; + private CompletionProvider completionProvider; private static CopilotCore COPILOT_CORE_PLUGIN = null; @@ -60,6 +62,7 @@ void init() { LanguageServerWrapper wrapper = LanguageServiceAccessor.startLanguageServer(serverDef); this.copilotLanguageServer = new CopilotLanguageServerConnection(wrapper); + this.completionProvider = new CompletionProvider(this.copilotLanguageServer); this.authStatusManager = new AuthStatusManager(this.copilotLanguageServer); this.authStatusManager.checkStatus(); @@ -83,4 +86,8 @@ public AuthStatusManager getAuthStatusManager() { return authStatusManager; } + public CompletionProvider getCompletionProvider() { + return completionProvider; + } + } diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionData.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/completion/CompletionCollection.java similarity index 58% rename from com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionData.java rename to com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/completion/CompletionCollection.java index 3b1e0558..68ff19e0 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionData.java +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/completion/CompletionCollection.java @@ -1,30 +1,31 @@ -package com.microsoft.copilot.eclipse.ui.completion; +package com.microsoft.copilot.eclipse.core.completion; import java.util.List; +import org.eclipse.jdt.annotation.NonNull; + import com.microsoft.copilot.eclipse.core.lsp.protocol.CompletionItem; /** * A class to hold data for the inline completion. */ -public class CompletionData { - - /** - * A constant for an empty completion item. This is used to indicate that the completion is not available. Thus when - * clear the ghost text, we can set it to the rendering. - */ - public static final CompletionItem EMPTY_ITEM = new CompletionItem("", "", null, "", null, -1); +public class CompletionCollection { - private List items; + private List completions; private int index; - private int triggerOffset; + private String uriString; /** * Creates a new CompletionData. */ - public CompletionData() { + public CompletionCollection(@NonNull List completions, String uriString) { + if (completions == null || completions.isEmpty()) { + throw new IllegalArgumentException("completions cannot be null or empty"); + // TODO: log & send telemetry + } + this.completions = completions; + this.uriString = uriString; this.index = 0; - this.triggerOffset = -1; } /** @@ -68,37 +69,21 @@ public String getRemainingLines() { * Get the number of items in the completion list. */ public int getSize() { - if (items == null) { - return 0; - } - return items.size(); + return this.completions.size(); } /** * Get the document version when the completion was triggered. */ public int getDocumentVersion() { - CompletionItem item = getCurrentItem(); - if (item == null) { - return -1; + if (this.completions.isEmpty()) { + throw new IllegalStateException("completions cannot be empty"); } - return item.getDocVersion(); + return this.completions.get(0).getDocVersion(); } - /** - * Set the completion items. - */ - public void setItems(List items) { - this.items = items; - this.index = 0; - } - - public int getTriggerOffset() { - return triggerOffset; - } - - public void setTriggerOffset(int offset) { - this.triggerOffset = offset; + public String getUriString() { + return this.uriString; } /** @@ -112,10 +97,7 @@ public int getNumberOfLines() { * Get the current active completion item. */ CompletionItem getCurrentItem() { - if (items == null || items.isEmpty() || index >= items.size()) { - return null; - } - return items.get(index); + return this.completions.get(index); } } diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionJob.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/completion/CompletionJob.java similarity index 77% rename from com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionJob.java rename to com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/completion/CompletionJob.java index a38bd213..5b1972c0 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionJob.java +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/completion/CompletionJob.java @@ -1,5 +1,6 @@ -package com.microsoft.copilot.eclipse.ui.completion; +package com.microsoft.copilot.eclipse.core.completion; +import java.util.Objects; import java.util.concurrent.ExecutionException; import org.eclipse.core.runtime.IProgressMonitor; @@ -17,31 +18,37 @@ */ public class CompletionJob extends Job { + public static final String COMPLETION_JOB_FAMILY = "com.microsoft.copilot.eclipse.completionJobFamily"; + private CompletionResult result; + private CopilotLanguageServerConnection lsConnection; + private CompletionParams params; + /** * Creates a new completion job. */ public CompletionJob(CopilotLanguageServerConnection lsConnection) { super("Generating completion..."); this.lsConnection = lsConnection; - this.setUser(true); + this.setSystem(true); + this.setPriority(Job.INTERACTIVE); } - private CopilotLanguageServerConnection lsConnection; - private CompletionParams params; - public void setCompletionParams(CompletionParams params) { this.params = params; } + public CompletionParams getCompletionParams() { + return params; + } + @Override protected IStatus run(IProgressMonitor monitor) { if (params == null) { return new Status(IStatus.ERROR, Constants.PLUGIN_ID, "Invalid completion parameters"); } if (monitor.isCanceled()) { - System.out.println("Completion job is canceled"); return Status.CANCEL_STATUS; } try { @@ -53,7 +60,6 @@ protected IStatus run(IProgressMonitor monitor) { return new Status(IStatus.ERROR, Constants.PLUGIN_ID, e.getMessage(), e); } if (monitor.isCanceled()) { - System.out.println("Completion job is canceled"); return Status.CANCEL_STATUS; } return Status.OK_STATUS; @@ -63,4 +69,13 @@ public CompletionResult getCompletionResult() { return result; } + public String getUriString() { + return params.getDoc().getUri(); + } + + @Override + public boolean belongsTo(Object family) { + return Objects.equals(family, COMPLETION_JOB_FAMILY); + } + } diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/completion/CompletionListener.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/completion/CompletionListener.java new file mode 100644 index 00000000..f63c6100 --- /dev/null +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/completion/CompletionListener.java @@ -0,0 +1,13 @@ +package com.microsoft.copilot.eclipse.core.completion; + +/** + * Listener for completion resolution. + */ +public interface CompletionListener { + + /** + * Notifies to the listeners when the completion is resolved. + */ + void onCompletionResolved(CompletionCollection completions); + +} diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/completion/CompletionProvider.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/completion/CompletionProvider.java new file mode 100644 index 00000000..5b362169 --- /dev/null +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/completion/CompletionProvider.java @@ -0,0 +1,123 @@ +package com.microsoft.copilot.eclipse.core.completion; + +import java.util.LinkedHashSet; +import java.util.Set; + +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.jobs.IJobChangeEvent; +import org.eclipse.core.runtime.jobs.IJobChangeListener; +import org.eclipse.lsp4j.Position; + +import com.microsoft.copilot.eclipse.core.lsp.CopilotLanguageServerConnection; +import com.microsoft.copilot.eclipse.core.lsp.protocol.CompletionDocument; +import com.microsoft.copilot.eclipse.core.lsp.protocol.CompletionParams; +import com.microsoft.copilot.eclipse.core.lsp.protocol.CompletionResult; + +/** + * Provider for inline completion. + */ +public class CompletionProvider implements IJobChangeListener { + + private CompletionJob completionJob; + + private Set completionListeners; + + /** + * Creates a new completion provider. + */ + public CompletionProvider(CopilotLanguageServerConnection lsConnection) { + this.completionJob = new CompletionJob(lsConnection); + this.completionJob.addJobChangeListener(this); + this.completionListeners = new LinkedHashSet<>(); + } + + /** + * Trigger an inline completion. + * + * @param uriString the URI string of the document. + * @param position the position of the cursor. + * @param documentVersion the version of the document. + */ + public void triggerCompletion(String uriString, Position position, int documentVersion) { + this.completionJob.cancel(); + this.completionJob.setCompletionParams(null); + CompletionDocument completionDoc = new CompletionDocument(uriString, position); + completionDoc.setVersion(documentVersion); + // following format options are hard-coded, because eclipse support applying the format options + // automatically when drawing text into the editor, so don't need to set the actual values here. + completionDoc.setInsertSpaces(true); + completionDoc.setTabSize(4); + CompletionParams params = new CompletionParams(completionDoc); + + this.completionJob.setCompletionParams(params); + this.completionJob.schedule(); + } + + /** + * Add a completion listener. + */ + public void addCompletionListener(CompletionListener listener) { + this.completionListeners.add(listener); + } + + /** + * Remove a completion listener. + */ + public void removeCompletionListener(CompletionListener listener) { + this.completionListeners.remove(listener); + } + + @Override + public void aboutToRun(IJobChangeEvent event) { + // do nothing + + } + + @Override + public void awake(IJobChangeEvent event) { + // do nothing + + } + + @Override + public void done(IJobChangeEvent event) { + IStatus jobStatus = this.completionJob.getResult(); + if (jobStatus != null && !jobStatus.isOK()) { + return; + // TODO: log & send telemetry + } + CompletionResult result = this.completionJob.getCompletionResult(); + if (result == null || result.getCompletions() == null || result.getCompletions().isEmpty()) { + return; + } + + CompletionParams params = this.completionJob.getCompletionParams(); + if (params == null) { + return; + } + + CompletionCollection completions = new CompletionCollection(result.getCompletions(), params.getDoc().getUri()); + for (CompletionListener listener : this.completionListeners) { + listener.onCompletionResolved(completions); + } + } + + @Override + public void running(IJobChangeEvent event) { + // do nothing + + } + + @Override + public void scheduled(IJobChangeEvent event) { + // do nothing + + } + + @Override + public void sleeping(IJobChangeEvent event) { + // do nothing + + } + +} diff --git a/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/completion/CompletionDataTests.java b/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/completion/CompletionDataTests.java deleted file mode 100644 index c3312f47..00000000 --- a/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/completion/CompletionDataTests.java +++ /dev/null @@ -1,60 +0,0 @@ -package com.microsoft.copilot.eclipse.ui.completion; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -import java.util.List; - -import org.junit.jupiter.api.Test; - -import com.microsoft.copilot.eclipse.core.lsp.protocol.CompletionItem; - -class CompletionDataTests { - - @Test - void testGetEmptyStringWhenNoItems() { - CompletionData data = new CompletionData(); - assertEquals("", data.getText()); - } - - @Test - void testGetFirstLineWithSingleLineItem() { - CompletionData data = new CompletionData(); - data.setItems(List.of(new CompletionItem("uuid", "first", null, "first", null, 0))); - assertEquals("first", data.getFirstLine()); - } - - @Test - void testGetRemainingLineWithSingleLineItem() { - CompletionData data = new CompletionData(); - data.setItems(List.of(new CompletionItem("uuid", "first", null, "first", null, 0))); - assertEquals("", data.getRemainingLines()); - } - - @Test - void testGetFirstLineWithMultilineItem() { - CompletionData data = new CompletionData(); - data.setItems(List.of(new CompletionItem("uuid", "first\nsecond\nthird", null, "first\nsecond\nthird", null, 0))); - assertEquals("first", data.getFirstLine()); - } - - @Test - void testGetRemainingLinesWithMultilineItem() { - CompletionData data = new CompletionData(); - data.setItems(List.of(new CompletionItem("uuid", "first\nsecond\nthird", null, "first\nsecond\nthird", null, 0))); - assertEquals("second\nthird", data.getRemainingLines()); - } - - @Test - void getNumberOfLinesWhenNoItems() { - CompletionData completionData = new CompletionData(); - assertEquals(1, completionData.getNumberOfLines()); - } - - @Test - void getNumberOfLinesWithMultilineItem() { - CompletionData data = new CompletionData(); - data.setItems(List.of(new CompletionItem("uuid", "first\nsecond\nthird", null, "first\nsecond\nthird", null, 0))); - assertEquals(3, data.getNumberOfLines()); - } - -} diff --git a/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/completion/CompletionJobTests.java b/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/completion/CompletionJobTests.java index 5c0f848c..d1fa4362 100644 --- a/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/completion/CompletionJobTests.java +++ b/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/completion/CompletionJobTests.java @@ -13,6 +13,7 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.junit.jupiter.MockitoExtension; +import com.microsoft.copilot.eclipse.core.completion.CompletionJob; import com.microsoft.copilot.eclipse.core.lsp.CopilotLanguageServerConnection; import com.microsoft.copilot.eclipse.core.lsp.protocol.CompletionDocument; import com.microsoft.copilot.eclipse.core.lsp.protocol.CompletionParams; diff --git a/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/completion/EditorManagerTests.java b/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/completion/EditorManagerTests.java index de006db2..462db6cc 100644 --- a/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/completion/EditorManagerTests.java +++ b/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/completion/EditorManagerTests.java @@ -7,29 +7,52 @@ import org.eclipse.ui.texteditor.ITextEditor; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import com.microsoft.copilot.eclipse.core.completion.CompletionProvider; import com.microsoft.copilot.eclipse.core.lsp.CopilotLanguageServerConnection; @ExtendWith(MockitoExtension.class) class EditorManagerTests { + @Mock + private CopilotLanguageServerConnection mockServer; + + @Mock + private CompletionProvider mockProvider; + @Test void testCreateHandlerForNull() { - CopilotLanguageServerConnection mockServer = mock(CopilotLanguageServerConnection.class); - EditorsManager manager = new EditorsManager(mockServer); + EditorsManager manager = new EditorsManager(mockServer, mockProvider); assertNull(manager.getOrCreateCompletionHandlerFor(null)); } @Test - void getOrCreateCompletionHandlerForReturnsNewHandlerWhenNotPresent() { + void testGetOrCreateCompletionHandlerForReturnsNewHandlerWhenNotPresent() { ITextEditor mockEditor = mock(ITextEditor.class); - CopilotLanguageServerConnection mockServer = mock(CopilotLanguageServerConnection.class); - EditorsManager manager = new EditorsManager(mockServer); + EditorsManager manager = new EditorsManager(mockServer, mockProvider); CompletionHandler handler = manager.getOrCreateCompletionHandlerFor(mockEditor); assertNotNull(handler); } + + @Test + void testGetActiveHandlerWhenNoActiveEditor() { + EditorsManager manager = new EditorsManager(mockServer, mockProvider); + + assertNull(manager.getActiveCompletionHandler()); + } + + @Test + void testGetActiveHandlerWhenActiveEditor() { + ITextEditor mockEditor = mock(ITextEditor.class); + EditorsManager manager = new EditorsManager(mockServer, mockProvider); + manager.getOrCreateCompletionHandlerFor(mockEditor); + manager.setActiveEditor(mockEditor); + + assertNotNull(manager.getActiveCompletionHandler()); + } } diff --git a/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/handler/AcceptFullSuggestionHandlerTests.java b/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/handler/AcceptFullSuggestionHandlerTests.java new file mode 100644 index 00000000..111b9918 --- /dev/null +++ b/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/handler/AcceptFullSuggestionHandlerTests.java @@ -0,0 +1,41 @@ +package com.microsoft.copilot.eclipse.ui.handler; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.when; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.MockedStatic; +import org.mockito.junit.jupiter.MockitoExtension; + +import com.microsoft.copilot.eclipse.ui.CopilotUi; +import com.microsoft.copilot.eclipse.ui.completion.CompletionHandler; +import com.microsoft.copilot.eclipse.ui.completion.EditorsManager; +import com.microsoft.copilot.eclipse.ui.handlers.AcceptFullSuggestionHandler; + +@ExtendWith(MockitoExtension.class) +class AcceptFullSuggestionHandlerTests { + + @Test + void testIsEnabledWhenNoCompletionIsAvailable() { + CopilotUi mockedUi = mock(CopilotUi.class); + EditorsManager mockedManager = mock(EditorsManager.class); + CompletionHandler mockedHandler = mock(CompletionHandler.class); + when(mockedUi.getEditorsManager()).thenReturn(mockedManager); + when(mockedManager.getActiveCompletionHandler()).thenReturn(mockedHandler); + when(mockedHandler.hasCompletion()).thenReturn(false); + + AcceptFullSuggestionHandler handler = new AcceptFullSuggestionHandler(); + + try (MockedStatic mockedStatic = mockStatic(CopilotUi.class)) { + mockedStatic.when(CopilotUi::getPlugin).thenReturn(mockedUi); + assertFalse(handler.isEnabled()); + } + + assertFalse(handler.isEnabled()); + + } + +} diff --git a/com.microsoft.copilot.eclipse.ui/.project b/com.microsoft.copilot.eclipse.ui/.project index 81edfba7..870b4b5d 100644 --- a/com.microsoft.copilot.eclipse.ui/.project +++ b/com.microsoft.copilot.eclipse.ui/.project @@ -21,12 +21,12 @@ - org.eclipse.m2e.core.maven2Builder + net.sf.eclipsecs.core.CheckstyleBuilder - net.sf.eclipsecs.core.CheckstyleBuilder + org.eclipse.m2e.core.maven2Builder diff --git a/com.microsoft.copilot.eclipse.ui/META-INF/MANIFEST.MF b/com.microsoft.copilot.eclipse.ui/META-INF/MANIFEST.MF index b02c80b6..267e163b 100644 --- a/com.microsoft.copilot.eclipse.ui/META-INF/MANIFEST.MF +++ b/com.microsoft.copilot.eclipse.ui/META-INF/MANIFEST.MF @@ -5,6 +5,7 @@ Bundle-SymbolicName: com.microsoft.copilot.eclipse.ui;singleton:=true Bundle-Version: 0.1.0.qualifier Export-Package: com.microsoft.copilot.eclipse.ui, com.microsoft.copilot.eclipse.ui.completion, + com.microsoft.copilot.eclipse.ui.handlers, com.microsoft.copilot.eclipse.ui.i18n Bundle-Activator: com.microsoft.copilot.eclipse.ui.CopilotUi Bundle-RequiredExecutionEnvironment: JavaSE-17 diff --git a/com.microsoft.copilot.eclipse.ui/plugin.properties b/com.microsoft.copilot.eclipse.ui/plugin.properties new file mode 100644 index 00000000..2ffc5fa2 --- /dev/null +++ b/com.microsoft.copilot.eclipse.ui/plugin.properties @@ -0,0 +1 @@ +command.acceptFullSuggestion.name=Accept Suggestions \ No newline at end of file diff --git a/com.microsoft.copilot.eclipse.ui/plugin.xml b/com.microsoft.copilot.eclipse.ui/plugin.xml index 099a09f4..a167123e 100644 --- a/com.microsoft.copilot.eclipse.ui/plugin.xml +++ b/com.microsoft.copilot.eclipse.ui/plugin.xml @@ -8,54 +8,73 @@ - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/CopilotUi.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/CopilotUi.java index 6c0a0d1d..01e47ce5 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/CopilotUi.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/CopilotUi.java @@ -1,10 +1,9 @@ package com.microsoft.copilot.eclipse.ui; +import org.eclipse.core.runtime.Plugin; import org.eclipse.ui.IEditorPart; import org.eclipse.ui.IWorkbenchWindow; import org.eclipse.ui.PlatformUI; -import org.eclipse.ui.texteditor.ITextEditor; -import org.osgi.framework.BundleActivator; import org.osgi.framework.BundleContext; import com.microsoft.copilot.eclipse.core.CopilotCore; @@ -16,12 +15,27 @@ /** * Activator class for the Copilot UI plugin. */ -public class CopilotUi implements BundleActivator { +public class CopilotUi extends Plugin { private static final int RETRY_COUNT = 30; + private static CopilotUi COPILOT_UI_PLUGIN = null; + private EditorLifecycleListener editorLifecycleListener; private EditorsManager editorsManager; + /** + * Creates the Copilot ui plugin. The plugin is created automatically by the Eclipse framework. Clients must not call + * this constructor. + */ + public CopilotUi() { + super(); + COPILOT_UI_PLUGIN = this; + } + + public static CopilotUi getPlugin() { + return COPILOT_UI_PLUGIN; + } + @Override public void start(BundleContext context) throws Exception { // wake up Core plugin and wait until copilot LS is ready @@ -39,14 +53,14 @@ public void start(BundleContext context) throws Exception { throw new IllegalStateException("Copilot language server is not ready."); } - this.editorsManager = new EditorsManager(connection); + this.editorsManager = new EditorsManager(connection, CopilotCore.getPlugin().getCompletionProvider()); this.editorLifecycleListener = new EditorLifecycleListener(editorsManager); registerPartListener(); // Initialize the completion handler for the active editor in case we miss the event // to initialize it. - initComletionHandlerForActiveEditor(); + initCompletionHandlerForActiveEditor(); } @Override @@ -57,6 +71,10 @@ public void stop(BundleContext context) throws Exception { } } + public EditorsManager getEditorsManager() { + return editorsManager; + } + private void registerPartListener() { IWorkbenchWindow[] windows = PlatformUI.getWorkbench().getWorkbenchWindows(); for (IWorkbenchWindow window : windows) { @@ -64,13 +82,10 @@ private void registerPartListener() { } } - private void initComletionHandlerForActiveEditor() { + private void initCompletionHandlerForActiveEditor() { IEditorPart editorPart = SwtUtils.getActiveEditorPart(); if (editorPart != null) { - ITextEditor textEditor = editorPart.getAdapter(ITextEditor.class); - if (textEditor != null) { - this.editorsManager.getOrCreateCompletionHandlerFor(textEditor); - } + this.editorLifecycleListener.createCompletionHandlerFor(editorPart); } } diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/UiConstants.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/UiConstants.java index b30d9d79..f6df7406 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/UiConstants.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/UiConstants.java @@ -1,7 +1,5 @@ package com.microsoft.copilot.eclipse.ui; -import org.eclipse.swt.graphics.RGB; - /** * A class to hold all the public constants used in the GitHub Copilot UI. */ @@ -15,7 +13,8 @@ private UiConstants() { public static final int TOOLBAR_ICON_HEIGHT_IN_PIEXL = 16; /** - * Default color for ghost text. + * Default color scale for ghost text. */ - public static final RGB DEFAULT_GHOST_TEXT_COLOR = new RGB(112, 112, 112); + + public static final int DEFAULT_GHOST_TEXT_SCALE = 112; } diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionHandler.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionHandler.java index aa178773..618c0738 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionHandler.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionHandler.java @@ -2,63 +2,66 @@ import java.io.IOException; import java.net.URI; -import java.util.List; -import org.eclipse.core.runtime.IStatus; -import org.eclipse.core.runtime.jobs.IJobChangeEvent; -import org.eclipse.core.runtime.jobs.IJobChangeListener; import org.eclipse.jface.text.BadLocationException; +import org.eclipse.jface.text.BadPositionCategoryException; +import org.eclipse.jface.text.DefaultPositionUpdater; import org.eclipse.jface.text.IDocument; import org.eclipse.jface.text.ITextListener; import org.eclipse.jface.text.ITextOperationTarget; import org.eclipse.jface.text.ITextViewer; import org.eclipse.jface.text.TextEvent; +import org.eclipse.jface.text.TextSelection; import org.eclipse.lsp4e.LSPEclipseUtils; -import org.eclipse.lsp4j.Position; import org.eclipse.swt.custom.CaretEvent; import org.eclipse.swt.custom.CaretListener; import org.eclipse.ui.texteditor.ITextEditor; +import com.microsoft.copilot.eclipse.core.completion.CompletionProvider; import com.microsoft.copilot.eclipse.core.lsp.CopilotLanguageServerConnection; -import com.microsoft.copilot.eclipse.core.lsp.protocol.CompletionDocument; -import com.microsoft.copilot.eclipse.core.lsp.protocol.CompletionParams; -import com.microsoft.copilot.eclipse.core.lsp.protocol.CompletionResult; import com.microsoft.copilot.eclipse.ui.utils.SwtUtils; import com.microsoft.copilot.eclipse.ui.utils.UiUtils; /** - * Handle completion for an ITextEditor. + * A class to listen events which are completion related and notify the completion manager to render the ghost text or + * apply the suggestion to document. */ -public class CompletionHandler implements ITextListener, CaretListener, IJobChangeListener { +public class CompletionHandler implements ITextListener, CaretListener { private CopilotLanguageServerConnection lsConnection; - private ITextEditor editor; + private CompletionProvider provider; private ITextViewer textViewer; private IDocument document; private URI documentUri; - private volatile int documentVersion; + private int documentVersion; + private org.eclipse.jface.text.Position triggerPosition; - private CompletionJob completionJob; - private CompletionData completionData; - private CompletionRendering completionRendering; + private DefaultPositionUpdater positionUpdater; + private CompletionManager completionManager; /** * Creates a new completion handler. */ - public CompletionHandler(CopilotLanguageServerConnection lsConnection, ITextEditor editor) { + public CompletionHandler(CopilotLanguageServerConnection lsConnection, CompletionProvider provider, + ITextEditor editor) { this.lsConnection = lsConnection; - this.completionJob = new CompletionJob(lsConnection); - this.completionData = new CompletionData(); - this.completionJob.addJobChangeListener(this); - this.editor = editor; - this.textViewer = (ITextViewer) this.editor.getAdapter(ITextOperationTarget.class); + this.textViewer = (ITextViewer) editor.getAdapter(ITextOperationTarget.class); + // if the text viewer is null, we will not register listeners. + // the side effect is that the completion will not be triggered for this editor. if (textViewer == null) { // TODO: log & send telemetry return; } - this.completionRendering = new CompletionRendering(this.textViewer, this.completionData); this.document = LSPEclipseUtils.getDocument(editor); - this.documentUri = UiUtils.getUriFromTextEditor(editor); + if (this.document == null) { + // TODO: log & send telemetry + return; + } + this.documentUri = LSPEclipseUtils.toUri(document); + if (this.documentUri == null) { + // TODO: log & send telemetry + return; + } try { lsConnection.connectDocument(this.document); } catch (IOException e) { @@ -66,17 +69,46 @@ public CompletionHandler(CopilotLanguageServerConnection lsConnection, ITextEdit return; } this.documentVersion = -1; + this.triggerPosition = new org.eclipse.jface.text.Position(0); + this.completionManager = new CompletionManager(lsConnection, provider, this.textViewer, this.document, + this.documentUri); registerListeners(); + + // position updater is used to update the position when the document is changed. + // this is needed because the completion ghost text is rendered based on the + // position in the document. If the document is changed, the position will be + // invalidated. + this.positionUpdater = new DefaultPositionUpdater(this.getCategory()); + this.document.addPositionCategory(this.getCategory()); + this.document.addPositionUpdater(this.positionUpdater); } - void registerListeners() { - // if the text viewer is null, we will not register listeners. - // the side effect is that the completion will not be triggered for this editor. - if (this.textViewer == null) { + /** + * Check if the completion handler has any completion suggestions. + */ + public boolean hasCompletion() { + return this.completionManager.hasCompletion(); + } + + /** + * Accept the full completion suggestion. + */ + public void acceptFullSuggestion() { + try { + this.document.addPosition(this.triggerPosition); + this.completionManager.acceptSuggestion(); + this.document.removePosition(this.triggerPosition); + } catch (BadLocationException e) { // TODO: log & send telemetry return; } + this.clearCompletionRendering(); + SwtUtils.invokeOnDisplayThread(() -> { + this.textViewer.getSelectionProvider().setSelection(new TextSelection(this.triggerPosition.offset, 0)); + }, this.textViewer.getTextWidget()); + } + void registerListeners() { SwtUtils.invokeOnDisplayThread(() -> { this.textViewer.getTextWidget().addCaretListener(this); this.textViewer.addTextListener(this); @@ -84,38 +116,11 @@ void registerListeners() { } void triggerCompletion() { - int caretOffset = UiUtils.getCaretOffset(this.editor); - this.completionData.setTriggerOffset(caretOffset); - CompletionParams completionParam = null; - try { - completionParam = this.createCompletionParams(caretOffset); - } catch (BadLocationException e) { - // TODO: log & send telemetry - return; - } - this.completionJob.cancel(); - this.completionJob.setCompletionParams(completionParam); - this.completionJob.schedule(); + this.completionManager.triggerCompletion(this.triggerPosition, this.documentVersion); } - CompletionParams createCompletionParams(int offset) throws BadLocationException { - String uriString = this.documentUri.toASCIIString(); - Position position = LSPEclipseUtils.toPosition(offset, this.document); - CompletionDocument completionDoc = new CompletionDocument(uriString, position); - completionDoc.setVersion(this.documentVersion); - // following format options are hard-coded, because eclipse support applying the format options - // automatically when drawing text into the editor, so don't need to set the actual values here. - completionDoc.setInsertSpaces(true); - completionDoc.setTabSize(4); - return new CompletionParams(completionDoc); - } - - void clearCompletion() { - if (this.completionData.getSize() > 0) { - // if the completion data is not empty, clear it and trigger a redraw. - this.completionData.setItems(List.of(CompletionData.EMPTY_ITEM)); - this.completionRendering.redraw(); - } + void clearCompletionRendering() { + this.completionManager.clearGhostText(this.triggerPosition); } @Override @@ -132,9 +137,11 @@ public void caretMoved(CaretEvent event) { } if (currentVersion == this.documentVersion) { // if the caret position is changed without document version change, we should remove the ghost text. - clearCompletion(); + clearCompletionRendering(); } else { this.documentVersion = currentVersion; + int caretOffset = UiUtils.getCaretOffset(this.textViewer); + this.triggerPosition = new org.eclipse.jface.text.Position(caretOffset); triggerCompletion(); } @@ -148,66 +155,34 @@ public void textChanged(TextEvent event) { // TODO: check changed text } + /** + * Get category for the position updater of this document. + */ + private String getCategory() { + return "GCE-" + this.documentUri.toASCIIString(); + } + /** * Disposes the resources of this completion handler. */ public void dispose() { - this.completionJob.cancel(); - this.completionJob.removeJobChangeListener(this); + this.completionManager.dispose(); lsConnection.disconnectDocument(this.documentUri); + try { + this.document.removePositionCategory(this.getCategory()); + } catch (BadPositionCategoryException e) { + // TODO: log & send telemetry + } + this.document.removePositionUpdater(this.positionUpdater); if (this.textViewer != null) { SwtUtils.invokeOnDisplayThread(() -> { - this.textViewer.getTextWidget().removeCaretListener(this); + if (this.textViewer.getTextWidget() != null) { + this.textViewer.getTextWidget().removeCaretListener(this); + } this.textViewer.removeTextListener(this); - this.completionRendering.dispose(); }); } } - @Override - public void aboutToRun(IJobChangeEvent event) { - // do nothing - } - - @Override - public void awake(IJobChangeEvent event) { - // do nothing - } - - @Override - public void done(IJobChangeEvent event) { - IStatus jobStatus = this.completionJob.getResult(); - if (jobStatus != null && !jobStatus.isOK()) { - return; - // TODO: log & send telemetry - } - CompletionResult result = this.completionJob.getCompletionResult(); - if (result == null || result.getCompletions() == null || result.getCompletions().isEmpty()) { - return; - } - // ignore the result if the document version is out-dated. - if (this.documentVersion != this.completionData.getDocumentVersion()) { - return; - } - - this.completionData.setItems(result.getCompletions()); - this.completionRendering.redraw(); - } - - @Override - public void running(IJobChangeEvent event) { - // do nothing - } - - @Override - public void scheduled(IJobChangeEvent event) { - // do nothing - } - - @Override - public void sleeping(IJobChangeEvent event) { - // do nothing - } - } diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionManager.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionManager.java new file mode 100644 index 00000000..bcb9fb50 --- /dev/null +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionManager.java @@ -0,0 +1,200 @@ +package com.microsoft.copilot.eclipse.ui.completion; + +import java.net.URI; +import java.util.Objects; + +import org.apache.commons.lang3.StringUtils; +import org.eclipse.jface.text.BadLocationException; +import org.eclipse.jface.text.IDocument; +import org.eclipse.jface.text.ITextViewer; +import org.eclipse.jface.text.Position; +import org.eclipse.lsp4e.LSPEclipseUtils; +import org.eclipse.swt.custom.StyledText; +import org.eclipse.swt.events.PaintEvent; +import org.eclipse.swt.events.PaintListener; +import org.eclipse.swt.graphics.Color; +import org.eclipse.swt.graphics.GC; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.graphics.RGB; +import org.eclipse.swt.graphics.Rectangle; + +import com.microsoft.copilot.eclipse.core.completion.CompletionCollection; +import com.microsoft.copilot.eclipse.core.completion.CompletionListener; +import com.microsoft.copilot.eclipse.core.completion.CompletionProvider; +import com.microsoft.copilot.eclipse.core.lsp.CopilotLanguageServerConnection; +import com.microsoft.copilot.eclipse.ui.UiConstants; +import com.microsoft.copilot.eclipse.ui.utils.SwtUtils; + +/** + * A class to control completion rendering. + */ +public class CompletionManager implements CompletionListener, PaintListener { + + private CopilotLanguageServerConnection lsConnection; + private CompletionProvider provider; + private IDocument document; + private URI documentUri; + private CompletionCollection completions; + + private ITextViewer textViewer; + private Color ghostTextColor; + private Position triggerPosition; + + /** + * Creates a new CompletionManager. + */ + public CompletionManager(CopilotLanguageServerConnection lsConnection, CompletionProvider provider, + ITextViewer textViewer, IDocument document, URI documentUri) { + this.lsConnection = lsConnection; + this.provider = provider; + this.provider.addCompletionListener(this); + this.document = document; + this.documentUri = documentUri; + this.completions = null; + + this.triggerPosition = new org.eclipse.jface.text.Position(0); + this.textViewer = textViewer; + StyledText styledText = textViewer.getTextWidget(); + if (styledText != null) { + SwtUtils.invokeOnDisplayThread(() -> { + styledText.addPaintListener(this); + this.ghostTextColor = new Color(styledText.getDisplay(), new RGB(UiConstants.DEFAULT_GHOST_TEXT_SCALE, + UiConstants.DEFAULT_GHOST_TEXT_SCALE, UiConstants.DEFAULT_GHOST_TEXT_SCALE)); + }, styledText); + } + } + + /** + * Triggers the completion. + */ + public void triggerCompletion(Position position, int documentVersion) { + this.triggerPosition = position; + try { + this.provider.triggerCompletion(documentUri.toASCIIString(), + LSPEclipseUtils.toPosition(position.getOffset(), this.document), documentVersion); + } catch (BadLocationException e) { + // TODO log & send telemetry + } + } + + /** + * Clear the completion. + */ + public void clearGhostText(Position position) { + this.triggerPosition = position; + this.completions = null; + StyledText styledText = textViewer.getTextWidget(); + if (styledText != null) { + SwtUtils.invokeOnDisplayThread(styledText::redraw, styledText); + } + + } + + @Override + public void onCompletionResolved(CompletionCollection completions) { + if (!Objects.equals(completions.getUriString(), this.documentUri.toASCIIString())) { + return; + } + + if (completions.getDocumentVersion() != this.lsConnection.getDocumentVersion(this.documentUri)) { + return; + } + + this.completions = completions; + StyledText styledText = textViewer.getTextWidget(); + if (styledText != null) { + SwtUtils.invokeOnDisplayThread(styledText::redraw, styledText); + } + } + + @Override + public void paintControl(PaintEvent e) { + StyledText styledText = textViewer.getTextWidget(); + if (styledText == null) { + return; + } + + GC gc = e.gc; + setLineVerticalIndentation(styledText, gc); + + if (this.completions == null) { + return; + } + + gc.setForeground(this.ghostTextColor); + // will get index out of bounds if the cursor is at the end. + // Because there is no more text to get bounds at EOF. + int caretOffset = Math.min(this.triggerPosition.getOffset(), styledText.getCharCount() - 1); + + String firstLine = this.completions.getFirstLine(); + if (StringUtils.isNotBlank(firstLine)) { + Rectangle bounds = styledText.getTextBounds(caretOffset, caretOffset); + int y = bounds.y; + y += bounds.height - styledText.getLineHeight(); + gc.drawString(firstLine, bounds.x + bounds.width, y, true); + } + String remainingLines = this.completions.getRemainingLines(); + if (StringUtils.isNotBlank(remainingLines)) { + int lineHeight = styledText.getLineHeight(); + int fontHeightt = gc.getFontMetrics().getHeight(); + int x = styledText.getLeftMargin(); + Point offsetLocation = styledText.getLocationAtOffset(caretOffset); + int y = offsetLocation.y + lineHeight * 2 - fontHeightt; + gc.drawText(remainingLines, x, y, true); + } + + } + + private void setLineVerticalIndentation(StyledText styledText, GC gc) { + int height = 0; + if (this.completions != null) { + // Change the height (line vertical indentation) to fit the line of + // ghost text. + Point ghostTextExtent = gc.textExtent(this.completions.getText()); + int numberOfLines = this.completions.getNumberOfLines(); + height = ghostTextExtent.y - ghostTextExtent.y / numberOfLines; + } + + int lineIndex = styledText.getLineAtOffset(this.triggerPosition.getOffset()) + 1; + lineIndex = Math.min(lineIndex, styledText.getLineCount() - 1); + styledText.setLineVerticalIndent(lineIndex, height); + } + + /** + * Return if the completion manager has completion rendering. + */ + public boolean hasCompletion() { + return this.completions != null; + } + + /** + * Apply the completion suggestion to document. + * + * @throws BadLocationException if the offset is invalid. + */ + public void acceptSuggestion() throws BadLocationException { + int offset = this.triggerPosition.offset; + if (offset < 0) { + return; + } + + String text = this.completions.getText(); + if (StringUtils.isEmpty(text)) { + return; + } + this.document.replace(offset, 0, text); + } + + /** + * Dispose the resources used by the completion manager. + */ + public void dispose() { + this.provider.removeCompletionListener(this); + this.completions = null; + if (this.ghostTextColor != null) { + this.ghostTextColor.dispose(); + this.ghostTextColor = null; + } + } + +} diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionRendering.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionRendering.java deleted file mode 100644 index 864b9615..00000000 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionRendering.java +++ /dev/null @@ -1,117 +0,0 @@ -package com.microsoft.copilot.eclipse.ui.completion; - -import org.apache.commons.lang3.StringUtils; -import org.eclipse.jface.text.ITextViewer; -import org.eclipse.swt.custom.StyledText; -import org.eclipse.swt.events.PaintEvent; -import org.eclipse.swt.events.PaintListener; -import org.eclipse.swt.graphics.Color; -import org.eclipse.swt.graphics.GC; -import org.eclipse.swt.graphics.Point; -import org.eclipse.swt.graphics.Rectangle; -import org.eclipse.swt.widgets.Display; - -import com.microsoft.copilot.eclipse.ui.UiConstants; -import com.microsoft.copilot.eclipse.ui.utils.SwtUtils; - -/** - * Render the ghost text for a completion item. - */ -public class CompletionRendering implements PaintListener { - - private ITextViewer textViewer; - private CompletionData completionData; - private Color ghostTextColor; - - /** - * Creates a new CompletionRendering. - */ - public CompletionRendering(ITextViewer textViewer, CompletionData completionData) { - this.completionData = completionData; - this.textViewer = textViewer; - StyledText styledText = textViewer.getTextWidget(); - if (styledText != null) { - SwtUtils.invokeOnDisplayThread(() -> { - styledText.addPaintListener(this); - this.ghostTextColor = new Color(Display.getCurrent(), UiConstants.DEFAULT_GHOST_TEXT_COLOR); - }); - } - - } - - public void setCompletionData(CompletionData completionData) { - this.completionData = completionData; - } - - @Override - public void paintControl(PaintEvent e) { - if (this.completionData == null || this.completionData.getTriggerOffset() < 0) { - return; - } - - StyledText styledText = textViewer.getTextWidget(); - if (styledText == null) { - return; - } - - GC gc = e.gc; - gc.setForeground(this.ghostTextColor); - // will get index out of bounds if the cursor is at the end. - // Because there is no more text to get bounds at EOF. - int caretOffset = Math.min(this.completionData.getTriggerOffset(), styledText.getCharCount() - 1); - String displayText = this.completionData.getText(); - - // set line vertical indentation - setLineVerticalIndentation(styledText, gc, caretOffset, displayText); - - String firstLine = this.completionData.getFirstLine(); - if (StringUtils.isNotBlank(firstLine)) { - Rectangle bounds = styledText.getTextBounds(caretOffset, caretOffset); - int y = bounds.y; - y += bounds.height - styledText.getLineHeight(); - gc.drawString(firstLine, bounds.x + bounds.width, y, true); - } - String remainingLines = this.completionData.getRemainingLines(); - if (StringUtils.isNotBlank(remainingLines)) { - int lineHt = styledText.getLineHeight(); - int fontHt = gc.getFontMetrics().getHeight(); - int x = styledText.getLeftMargin(); - Point offsetLocation = styledText.getLocationAtOffset(caretOffset); - int y = offsetLocation.y + lineHt * 2 - fontHt; - gc.drawText(remainingLines, x, y, true); - } - } - - /** - * Trigger a redraw event to update the ghost text. - */ - public void redraw() { - StyledText styledText = textViewer.getTextWidget(); - if (styledText != null) { - // TODO: can we use redrawRange() to improve the perf? - SwtUtils.invokeOnDisplayThread(() -> styledText.redraw()); - } - } - - /** - * Dispose the resources used by the rendering. - */ - public void dispose() { - if (this.ghostTextColor != null) { - this.ghostTextColor.dispose(); - this.ghostTextColor = null; - } - } - - private void setLineVerticalIndentation(StyledText styledText, GC gc, int caretOffset, String displayText) { - Point ghostTextExtent = gc.textExtent(displayText); - int numberOfLines = this.completionData.getNumberOfLines(); - if (numberOfLines <= 1) { - return; - } - int height = ghostTextExtent.y - ghostTextExtent.y / numberOfLines; - int lineIndex = styledText.getLineAtOffset(caretOffset); - styledText.setLineVerticalIndent(lineIndex + 1, height); - } - -} diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/EditorLifecycleListener.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/EditorLifecycleListener.java index 4aba250d..97857142 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/EditorLifecycleListener.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/EditorLifecycleListener.java @@ -44,12 +44,16 @@ public void partOpened(IWorkbenchPart part) { // do nothing. } - void createCompletionHandlerFor(IWorkbenchPart part) { + /** + * Creates the {@link CompletionHandler} for the ITextEditor of the IWorkbenchPart. + */ + public void createCompletionHandlerFor(IWorkbenchPart part) { ITextEditor editor = part.getAdapter(ITextEditor.class); if (editor == null) { return; } manager.getOrCreateCompletionHandlerFor(editor); + manager.setActiveEditor(editor); } void disposeCompletionHandlerFor(IWorkbenchPart part) { @@ -58,6 +62,9 @@ void disposeCompletionHandlerFor(IWorkbenchPart part) { return; } manager.disposeCompletionHandlerFor(editor); + if (editor.equals(manager.getActiveEditor())) { + manager.setActiveEditor(null); + } } } diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/EditorsManager.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/EditorsManager.java index f38e408c..25870934 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/EditorsManager.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/EditorsManager.java @@ -2,9 +2,12 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicReference; +import org.eclipse.jdt.annotation.Nullable; import org.eclipse.ui.texteditor.ITextEditor; +import com.microsoft.copilot.eclipse.core.completion.CompletionProvider; import com.microsoft.copilot.eclipse.core.lsp.CopilotLanguageServerConnection; /** @@ -13,14 +16,18 @@ public class EditorsManager { private CopilotLanguageServerConnection languageServer; + private CompletionProvider completionProvider; private Map editorMap; + private AtomicReference activeEditor; /** * Creates a new EditorManager. */ - public EditorsManager(CopilotLanguageServerConnection languageServer) { + public EditorsManager(CopilotLanguageServerConnection languageServer, CompletionProvider completionProvider) { this.languageServer = languageServer; + this.completionProvider = completionProvider; this.editorMap = new ConcurrentHashMap<>(); + this.activeEditor = new AtomicReference<>(); } /** @@ -33,7 +40,20 @@ public CompletionHandler getOrCreateCompletionHandlerFor(ITextEditor editor) { return null; } - return editorMap.computeIfAbsent(editor, edt -> new CompletionHandler(this.languageServer, edt)); + return editorMap.computeIfAbsent(editor, + edt -> new CompletionHandler(this.languageServer, this.completionProvider, edt)); + } + + /** + * Gets the {@link com.microsoft.copilot.eclipse.ui.completion.CompletionHandler CompletionHandler} for the active + * ITextEditor. + */ + @Nullable + public CompletionHandler getActiveCompletionHandler() { + if (this.activeEditor.get() == null) { + return null; + } + return this.editorMap.get(activeEditor.get()); } /** @@ -47,6 +67,18 @@ public void disposeCompletionHandlerFor(ITextEditor editor) { } } + /** + * Sets the active editor. + */ + public void setActiveEditor(ITextEditor editor) { + this.activeEditor.set(editor); + } + + @Nullable + public ITextEditor getActiveEditor() { + return this.activeEditor.get(); + } + /** * Dispose all the handlers. */ diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/AcceptFullSuggestionHandler.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/AcceptFullSuggestionHandler.java new file mode 100644 index 00000000..52e31a46 --- /dev/null +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/AcceptFullSuggestionHandler.java @@ -0,0 +1,30 @@ +package com.microsoft.copilot.eclipse.ui.handlers; + +import org.eclipse.core.commands.ExecutionEvent; +import org.eclipse.core.commands.ExecutionException; + +import com.microsoft.copilot.eclipse.ui.completion.CompletionHandler; + +/** + * Handler for accepting the full suggestion. + */ +public class AcceptFullSuggestionHandler extends CopilotHandler { + + @Override + public Object execute(ExecutionEvent event) throws ExecutionException { + CompletionHandler handler = getActiveCompletionHandler(); + if (handler != null) { + handler.acceptFullSuggestion(); + } + return null; + } + + @Override + public boolean isEnabled() { + CompletionHandler handler = getActiveCompletionHandler(); + if (handler != null) { + return handler.hasCompletion(); + } + return false; + } +} diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/CopilotHandler.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/CopilotHandler.java new file mode 100644 index 00000000..913fbbb8 --- /dev/null +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/CopilotHandler.java @@ -0,0 +1,29 @@ +package com.microsoft.copilot.eclipse.ui.handlers; + +import org.eclipse.core.commands.AbstractHandler; +import org.eclipse.jdt.annotation.Nullable; + +import com.microsoft.copilot.eclipse.ui.CopilotUi; +import com.microsoft.copilot.eclipse.ui.completion.CompletionHandler; +import com.microsoft.copilot.eclipse.ui.completion.EditorsManager; + +/** + * Base class for Copilot handlers. + */ +public abstract class CopilotHandler extends AbstractHandler { + /** + * Gets the active {@link CompletionHandler} for the current editor. + */ + @Nullable + public CompletionHandler getActiveCompletionHandler() { + CopilotUi copilotUi = CopilotUi.getPlugin(); + if (copilotUi == null) { + return null; + } + EditorsManager manager = copilotUi.getEditorsManager(); + if (manager == null) { + return null; + } + return manager.getActiveCompletionHandler(); + } +} diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/utils/UiUtils.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/utils/UiUtils.java index 95d0e2fa..69d91632 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/utils/UiUtils.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/utils/UiUtils.java @@ -1,20 +1,14 @@ package com.microsoft.copilot.eclipse.ui.utils; -import java.net.URI; import java.util.concurrent.atomic.AtomicInteger; -import org.eclipse.core.resources.IFile; -import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.jface.text.ITextViewer; import org.eclipse.swt.custom.StyledText; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.graphics.ImageData; import org.eclipse.swt.graphics.ImageLoader; -import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Display; -import org.eclipse.ui.IEditorInput; -import org.eclipse.ui.IFileEditorInput; -import org.eclipse.ui.texteditor.ITextEditor; /** * Utilities for Eclipse UI. @@ -25,20 +19,6 @@ private UiUtils() { // prevent instantiation } - /** - * Gets the URI of the file opened in the given text editor. - */ - @Nullable - public static URI getUriFromTextEditor(ITextEditor editor) { - IEditorInput input = editor.getEditorInput(); - if (input instanceof IFileEditorInput fileInput) { - IFile file = fileInput.getFile(); - return file.getLocationURI(); - } - - return null; - } - /** * Resizes the icon at the given path to the given width and height. Icon size is 16x16 by default, which is the * recommended size for toolbar icons. For more details: https://eclipse-platform.github.io/ui-best-practices/#toolbar @@ -55,11 +35,11 @@ public static ImageDescriptor resizeIcon(String path, int width, int height) { } /** - * Gets the caret offset of the given text editor. + * Gets the caret offset of the given text viewer. */ - public static int getCaretOffset(ITextEditor editor) { + public static int getCaretOffset(ITextViewer textViewer) { final AtomicInteger ref = new AtomicInteger(0); - StyledText styledText = (StyledText) editor.getAdapter(Control.class); + StyledText styledText = textViewer.getTextWidget(); SwtUtils.invokeOnDisplayThread(() -> { int offset = styledText.getCaretOffset(); ref.set(offset); From 29a1092b08831b0aa356c4d3476d7a98988f97c4 Mon Sep 17 00:00:00 2001 From: Sheng Chen Date: Tue, 17 Dec 2024 15:35:36 +0800 Subject: [PATCH 021/690] feat - Support trigger inline completion and discard suggestions (#29) * feat - Support trigger inline completion and discard suggestions * Address comments --- .../copilot/eclipse/core/CopilotCore.java | 3 +- .../AcceptFullSuggestionHandlerTests.java | 5 +-- .../DiscardSuggestionHandlerTests.java | 38 +++++++++++++++++++ .../TriggerInlineSuggestionHandlerTests.java | 37 ++++++++++++++++++ .../plugin.properties | 4 +- com.microsoft.copilot.eclipse.ui/plugin.xml | 28 ++++++++++++++ .../copilot/eclipse/ui/CopilotUi.java | 2 +- .../ui/completion/CompletionHandler.java | 11 +++++- .../ui/handlers/DiscardSuggestionHandler.java | 29 ++++++++++++++ .../TriggerInlineSuggestionHandler.java | 31 +++++++++++++++ 10 files changed, 179 insertions(+), 9 deletions(-) create mode 100644 com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/handler/DiscardSuggestionHandlerTests.java create mode 100644 com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/handler/TriggerInlineSuggestionHandlerTests.java create mode 100644 com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/DiscardSuggestionHandler.java create mode 100644 com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/TriggerInlineSuggestionHandler.java diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/CopilotCore.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/CopilotCore.java index bbfbd6c0..6981e2bb 100644 --- a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/CopilotCore.java +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/CopilotCore.java @@ -14,7 +14,8 @@ import com.microsoft.copilot.eclipse.core.lsp.CopilotLanguageServerConnection; /** - * Activator class for the Copilot core plugin. + * The plug-in runtime class for the Copilot plug-in containing the core (UI-free) support, like the completion, + * authentication, language server connection, etc. */ public class CopilotCore extends Plugin { diff --git a/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/handler/AcceptFullSuggestionHandlerTests.java b/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/handler/AcceptFullSuggestionHandlerTests.java index 111b9918..eb57d598 100644 --- a/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/handler/AcceptFullSuggestionHandlerTests.java +++ b/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/handler/AcceptFullSuggestionHandlerTests.java @@ -19,7 +19,7 @@ class AcceptFullSuggestionHandlerTests { @Test - void testIsEnabledWhenNoCompletionIsAvailable() { + void testIsNotEnabledWhenNoCompletionIsAvailable() { CopilotUi mockedUi = mock(CopilotUi.class); EditorsManager mockedManager = mock(EditorsManager.class); CompletionHandler mockedHandler = mock(CompletionHandler.class); @@ -33,9 +33,6 @@ void testIsEnabledWhenNoCompletionIsAvailable() { mockedStatic.when(CopilotUi::getPlugin).thenReturn(mockedUi); assertFalse(handler.isEnabled()); } - - assertFalse(handler.isEnabled()); - } } diff --git a/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/handler/DiscardSuggestionHandlerTests.java b/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/handler/DiscardSuggestionHandlerTests.java new file mode 100644 index 00000000..1d9d8be3 --- /dev/null +++ b/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/handler/DiscardSuggestionHandlerTests.java @@ -0,0 +1,38 @@ +package com.microsoft.copilot.eclipse.ui.handler; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.when; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.MockedStatic; +import org.mockito.junit.jupiter.MockitoExtension; + +import com.microsoft.copilot.eclipse.ui.CopilotUi; +import com.microsoft.copilot.eclipse.ui.completion.CompletionHandler; +import com.microsoft.copilot.eclipse.ui.completion.EditorsManager; +import com.microsoft.copilot.eclipse.ui.handlers.DiscardSuggestionHandler; + +@ExtendWith(MockitoExtension.class) +class DiscardSuggestionHandlerTests { + + @Test + void testIsNotEnabledWhenNoCompletionIsAvailable() { + CopilotUi mockedUi = mock(CopilotUi.class); + EditorsManager mockedManager = mock(EditorsManager.class); + CompletionHandler mockedHandler = mock(CompletionHandler.class); + when(mockedUi.getEditorsManager()).thenReturn(mockedManager); + when(mockedManager.getActiveCompletionHandler()).thenReturn(mockedHandler); + when(mockedHandler.hasCompletion()).thenReturn(false); + + DiscardSuggestionHandler handler = new DiscardSuggestionHandler(); + + try (MockedStatic mockedStatic = mockStatic(CopilotUi.class)) { + mockedStatic.when(CopilotUi::getPlugin).thenReturn(mockedUi); + assertFalse(handler.isEnabled()); + } + } + +} diff --git a/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/handler/TriggerInlineSuggestionHandlerTests.java b/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/handler/TriggerInlineSuggestionHandlerTests.java new file mode 100644 index 00000000..2aaec16e --- /dev/null +++ b/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/handler/TriggerInlineSuggestionHandlerTests.java @@ -0,0 +1,37 @@ +package com.microsoft.copilot.eclipse.ui.handler; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.when; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.MockedStatic; +import org.mockito.junit.jupiter.MockitoExtension; + +import com.microsoft.copilot.eclipse.ui.CopilotUi; +import com.microsoft.copilot.eclipse.ui.completion.CompletionHandler; +import com.microsoft.copilot.eclipse.ui.completion.EditorsManager; +import com.microsoft.copilot.eclipse.ui.handlers.TriggerInlineSuggestionHandler; + +@ExtendWith(MockitoExtension.class) +class TriggerInlineSuggestionHandlerTests { + + @Test + void testIsEnabledWhenNoCompletionIsAvailable() { + CopilotUi mockedUi = mock(CopilotUi.class); + EditorsManager mockedManager = mock(EditorsManager.class); + CompletionHandler mockedHandler = mock(CompletionHandler.class); + when(mockedUi.getEditorsManager()).thenReturn(mockedManager); + when(mockedManager.getActiveCompletionHandler()).thenReturn(mockedHandler); + when(mockedHandler.hasCompletion()).thenReturn(false); + + TriggerInlineSuggestionHandler handler = new TriggerInlineSuggestionHandler(); + + try (MockedStatic mockedStatic = mockStatic(CopilotUi.class)) { + mockedStatic.when(CopilotUi::getPlugin).thenReturn(mockedUi); + assertTrue(handler.isEnabled()); + } + } +} diff --git a/com.microsoft.copilot.eclipse.ui/plugin.properties b/com.microsoft.copilot.eclipse.ui/plugin.properties index 2ffc5fa2..f2b1b840 100644 --- a/com.microsoft.copilot.eclipse.ui/plugin.properties +++ b/com.microsoft.copilot.eclipse.ui/plugin.properties @@ -1 +1,3 @@ -command.acceptFullSuggestion.name=Accept Suggestions \ No newline at end of file +command.triggerInlineSuggestions.name=Trigger Inline Suggestion +command.acceptFullSuggestion.name=Accept Suggestion +command.discardSuggestion.name=Discard Suggestion diff --git a/com.microsoft.copilot.eclipse.ui/plugin.xml b/com.microsoft.copilot.eclipse.ui/plugin.xml index a167123e..0e341bfd 100644 --- a/com.microsoft.copilot.eclipse.ui/plugin.xml +++ b/com.microsoft.copilot.eclipse.ui/plugin.xml @@ -48,10 +48,18 @@ name="Sign out from GitHub Copilot" style="push"> + + + + @@ -61,20 +69,40 @@ class="com.microsoft.copilot.eclipse.ui.handlers.ShowStatusBarMenuHandler" commandId="com.microsoft.copilot.eclipse.commands.showStatusBarMenu"> + + + + + + + + diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/CopilotUi.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/CopilotUi.java index 01e47ce5..9a36a17b 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/CopilotUi.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/CopilotUi.java @@ -13,7 +13,7 @@ import com.microsoft.copilot.eclipse.ui.utils.SwtUtils; /** - * Activator class for the Copilot UI plugin. + * The plug-in runtime class for the Copilot plug-in containing the UI support, like dialogs, ghost text rendering, etc. */ public class CopilotUi extends Plugin { diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionHandler.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionHandler.java index 618c0738..825a3c51 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionHandler.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionHandler.java @@ -115,11 +115,18 @@ void registerListeners() { }); } - void triggerCompletion() { + /** + * Trigger the inline completion. + */ + public void triggerCompletion() { + clearCompletionRendering(); this.completionManager.triggerCompletion(this.triggerPosition, this.documentVersion); } - void clearCompletionRendering() { + /** + * Clear the completion ghost text. + */ + public void clearCompletionRendering() { this.completionManager.clearGhostText(this.triggerPosition); } diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/DiscardSuggestionHandler.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/DiscardSuggestionHandler.java new file mode 100644 index 00000000..aa46caf8 --- /dev/null +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/DiscardSuggestionHandler.java @@ -0,0 +1,29 @@ +package com.microsoft.copilot.eclipse.ui.handlers; + +import org.eclipse.core.commands.ExecutionEvent; +import org.eclipse.core.commands.ExecutionException; + +import com.microsoft.copilot.eclipse.ui.completion.CompletionHandler; + +/** + * Handler for clearing the completion ghost text. + */ +public class DiscardSuggestionHandler extends CopilotHandler { + @Override + public Object execute(ExecutionEvent event) throws ExecutionException { + CompletionHandler handler = getActiveCompletionHandler(); + if (handler != null) { + handler.clearCompletionRendering(); + } + return null; + } + + @Override + public boolean isEnabled() { + CompletionHandler handler = getActiveCompletionHandler(); + if (handler != null) { + return handler.hasCompletion(); + } + return false; + } +} diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/TriggerInlineSuggestionHandler.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/TriggerInlineSuggestionHandler.java new file mode 100644 index 00000000..1315f87a --- /dev/null +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/TriggerInlineSuggestionHandler.java @@ -0,0 +1,31 @@ +package com.microsoft.copilot.eclipse.ui.handlers; + +import org.eclipse.core.commands.ExecutionEvent; +import org.eclipse.core.commands.ExecutionException; + +import com.microsoft.copilot.eclipse.ui.completion.CompletionHandler; + +/** + * Handler for triggering the inline suggestion. + */ +public class TriggerInlineSuggestionHandler extends CopilotHandler { + + @Override + public Object execute(ExecutionEvent event) throws ExecutionException { + CompletionHandler handler = getActiveCompletionHandler(); + if (handler != null) { + handler.triggerCompletion(); + } + return null; + } + + @Override + public boolean isEnabled() { + CompletionHandler handler = getActiveCompletionHandler(); + if (handler != null) { + return !handler.hasCompletion(); + } + return false; + } + +} From ffc91797e75c12d4405c61f7961f677560ee0f85 Mon Sep 17 00:00:00 2001 From: ethanyhou <149548697+ethanyhou@users.noreply.github.com> Date: Wed, 18 Dec 2024 15:49:18 +0800 Subject: [PATCH 022/690] feat - Enable sign in/out to the plugin status bar menu. (#28) --- .../eclipse/core/AuthStatusManagerTests.java | 37 +++++ .../core/utils/PlatformUtilsTests.java | 19 +++ .../META-INF/MANIFEST.MF | 3 +- .../eclipse/core/AuthStatusManager.java | 59 +++++++- .../core/lsp/CopilotLanguageServer.java | 21 +++ .../lsp/CopilotLanguageServerConnection.java | 37 +++++ .../core/lsp/protocol/AuthStatusResult.java | 5 + .../eclipse/core/lsp/protocol/NullParams.java | 8 ++ .../lsp/protocol/SignInConfirmParams.java | 56 ++++++++ .../lsp/protocol/SignInInitiateResult.java | 102 ++++++++++++++ .../eclipse/core/utils/PlatformUtils.java | 16 +++ .../copilot/eclipse/ui/i18n/MessagesTest.java | 4 +- .../META-INF/MANIFEST.MF | 4 +- .../plugin.properties | 3 + com.microsoft.copilot.eclipse.ui/plugin.xml | 39 +++-- .../ui/dialogs/SignInConfirmDialog.java | 133 ++++++++++++++++++ .../eclipse/ui/dialogs/SignInDialog.java | 100 +++++++++++++ .../ui/handlers/ShowStatusBarMenuHandler.java | 106 ++++++++++---- .../eclipse/ui/handlers/SignInHandler.java | 116 +++++++++++++++ .../eclipse/ui/handlers/SignOutHandler.java | 64 +++++++++ .../copilot/eclipse/ui/i18n/Messages.java | 34 ++++- .../eclipse/ui/i18n/messages.properties | 34 ++++- .../copilot/eclipse/ui/utils/SwtUtils.java | 26 ++++ .../copilot/eclipse/ui/utils/UiUtils.java | 42 +++++- 24 files changed, 1009 insertions(+), 59 deletions(-) create mode 100644 com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/utils/PlatformUtilsTests.java create mode 100644 com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/NullParams.java create mode 100644 com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/SignInConfirmParams.java create mode 100644 com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/SignInInitiateResult.java create mode 100644 com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/dialogs/SignInConfirmDialog.java create mode 100644 com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/dialogs/SignInDialog.java create mode 100644 com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/SignInHandler.java create mode 100644 com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/SignOutHandler.java diff --git a/com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/AuthStatusManagerTests.java b/com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/AuthStatusManagerTests.java index 596e8416..416e1829 100644 --- a/com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/AuthStatusManagerTests.java +++ b/com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/AuthStatusManagerTests.java @@ -4,6 +4,7 @@ import static org.mockito.Mockito.when; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -35,7 +36,43 @@ void testAuthStatusResultOnSuccess() { authStatusManager.checkStatus(); assertEquals(AuthStatusResult.OK, authStatusManager.getAuthStatusResult().getStatus()); + } + + @Test + void testCheckStatusLoadingWithDelay() throws InterruptedException { + String mockedUser = "mockedUser"; + // Arrange + AuthStatusResult expectedResult = new AuthStatusResult(); + expectedResult.setStatus(AuthStatusResult.OK); + expectedResult.setUser(mockedUser); + CompletableFuture future = new CompletableFuture<>(); + + when(mockConnection.checkStatus(false)).thenReturn(future); + + // Act + authStatusManager.checkStatus(); + + // Assert initial status is LOADING + assertEquals(AuthStatusResult.LOADING, authStatusManager.getAuthStatusResult().getStatus()); + + + future.complete(expectedResult); + + // Assert final status is OK + assertEquals(AuthStatusResult.OK, authStatusManager.getAuthStatusResult().getStatus()); + assertEquals(mockedUser, authStatusManager.getAuthStatusResult().getUser()); + } + + @Test + void testCheckStatusError() { + CompletableFuture future = new CompletableFuture<>(); + future.completeExceptionally(new CompletionException(new Exception("Some other error"))); + + when(mockConnection.checkStatus(false)).thenReturn(future); + + authStatusManager.checkStatus(); + assertEquals(AuthStatusResult.ERROR, authStatusManager.getAuthStatusResult().getStatus()); } } diff --git a/com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/utils/PlatformUtilsTests.java b/com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/utils/PlatformUtilsTests.java new file mode 100644 index 00000000..606f8544 --- /dev/null +++ b/com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/utils/PlatformUtilsTests.java @@ -0,0 +1,19 @@ +package com.microsoft.copilot.eclipse.core.utils; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + + +class PlatformUtilsTests { + + @Test + void testEscapeSpaceInUrl() { + assertEquals("https://example.com/path%20with%20spaces", + PlatformUtils.escapeSpaceInUrl("https://example.com/path with spaces")); + assertEquals("https://example.com/nospaces", PlatformUtils.escapeSpaceInUrl("https://example.com/nospaces")); + assertEquals("%20leading%20and%20trailing%20", PlatformUtils.escapeSpaceInUrl(" leading and trailing ")); + assertEquals("", PlatformUtils.escapeSpaceInUrl("")); + assertEquals("%20", PlatformUtils.escapeSpaceInUrl(" ")); + } +} diff --git a/com.microsoft.copilot.eclipse.core/META-INF/MANIFEST.MF b/com.microsoft.copilot.eclipse.core/META-INF/MANIFEST.MF index 21c7a7ce..bdcc5012 100644 --- a/com.microsoft.copilot.eclipse.core/META-INF/MANIFEST.MF +++ b/com.microsoft.copilot.eclipse.core/META-INF/MANIFEST.MF @@ -6,7 +6,8 @@ Bundle-Version: 0.1.0.qualifier Export-Package: com.microsoft.copilot.eclipse.core, com.microsoft.copilot.eclipse.core.completion, com.microsoft.copilot.eclipse.core.lsp, - com.microsoft.copilot.eclipse.core.lsp.protocol + com.microsoft.copilot.eclipse.core.lsp.protocol, + com.microsoft.copilot.eclipse.core.utils Bundle-Activator: com.microsoft.copilot.eclipse.core.CopilotCore Bundle-RequiredExecutionEnvironment: JavaSE-17 Automatic-Module-Name: com.microsoft.copilot.eclipse.core diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/AuthStatusManager.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/AuthStatusManager.java index 22cb4e6e..2a0cc3ff 100644 --- a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/AuthStatusManager.java +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/AuthStatusManager.java @@ -1,7 +1,12 @@ package com.microsoft.copilot.eclipse.core; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; + import com.microsoft.copilot.eclipse.core.lsp.CopilotLanguageServerConnection; import com.microsoft.copilot.eclipse.core.lsp.protocol.AuthStatusResult; +import com.microsoft.copilot.eclipse.core.lsp.protocol.SignInInitiateResult; /** * Manager for the authentication status. @@ -12,6 +17,8 @@ public class AuthStatusManager { private AuthStatusResult authStatusResult; + private static final int CHECK_STATUS_TIMEOUT_MILLIS = 3000; + /** * Constructor for the AuthStatusManager. * @@ -20,23 +27,69 @@ public class AuthStatusManager { public AuthStatusManager(CopilotLanguageServerConnection connection) { this.connection = connection; this.authStatusResult = new AuthStatusResult(); - this.authStatusResult.setStatus(AuthStatusResult.NOT_SIGNED_IN); + this.authStatusResult.setStatus(AuthStatusResult.LOADING); + } + + /** + * Initiate the sign in process. + + * @throws ExecutionException if the sign in initiate process fails due to an execution error + * @throws InterruptedException if the sign in initiate process is interrupted + */ + public SignInInitiateResult signInInitiate() throws InterruptedException, ExecutionException { + SignInInitiateResult result = connection.signInInitiate().get(); + if (result.isAlreadySignedIn()) { + this.authStatusResult.setStatus(AuthStatusResult.OK); + } + return result; + } + + /** + * Confirm the sign in process. + + * @throws ExecutionException if the sign in process fails due to an execution error + * @throws InterruptedException if the sign in process is interrupted + */ + public AuthStatusResult signInConfirm(String userCode) throws InterruptedException, ExecutionException { + AuthStatusResult result = connection.signInConfirm(userCode).get(); + if (result.isSignedIn()) { + this.authStatusResult.setStatus(AuthStatusResult.OK); + this.authStatusResult.setUser(result.getUser()); + } + return result; + } + + /** + * Sign out from the GitHub Copilot. + + * @throws ExecutionException if the sign out process fails due to an execution error + * @throws InterruptedException if the sign out process is interrupted + */ + public AuthStatusResult signOut() throws InterruptedException, ExecutionException { + AuthStatusResult result = connection.signOut().get(); + if (!result.isSignedIn()) { + this.authStatusResult.setStatus(AuthStatusResult.NOT_SIGNED_IN); + } + return result; } /** * Check the login status for current machine. */ public void checkStatus() { - this.connection.checkStatus(false).thenAccept(result -> { + CompletableFuture statusFuture = this.connection.checkStatus(false); + + statusFuture.orTimeout(CHECK_STATUS_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS).thenAccept(result -> { this.authStatusResult = result; }).exceptionally(ex -> { // TODO: log & send telemetry this.authStatusResult.setStatus(AuthStatusResult.ERROR); + return null; }); } public AuthStatusResult getAuthStatusResult() { - return authStatusResult; + return this.authStatusResult; } } diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/CopilotLanguageServer.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/CopilotLanguageServer.java index 8b6e8e45..1b214768 100644 --- a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/CopilotLanguageServer.java +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/CopilotLanguageServer.java @@ -9,6 +9,9 @@ import com.microsoft.copilot.eclipse.core.lsp.protocol.CheckStatusParams; import com.microsoft.copilot.eclipse.core.lsp.protocol.CompletionParams; import com.microsoft.copilot.eclipse.core.lsp.protocol.CompletionResult; +import com.microsoft.copilot.eclipse.core.lsp.protocol.NullParams; +import com.microsoft.copilot.eclipse.core.lsp.protocol.SignInConfirmParams; +import com.microsoft.copilot.eclipse.core.lsp.protocol.SignInInitiateResult; /** * Interface for Copilot Language Server. @@ -27,4 +30,22 @@ public interface CopilotLanguageServer extends LanguageServer { @JsonRequest CompletableFuture getCompletions(CompletionParams params); + /** + * Initiate the sign in process. + */ + @JsonRequest + CompletableFuture signInInitiate(NullParams param); + + /** + * Confirm the sign in process. + */ + @JsonRequest + CompletableFuture signInConfirm(SignInConfirmParams param); + + /** + * Sign out the current user. + */ + @JsonRequest + CompletableFuture signOut(NullParams params); + } diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/CopilotLanguageServerConnection.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/CopilotLanguageServerConnection.java index ac1d23b1..47c46be1 100644 --- a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/CopilotLanguageServerConnection.java +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/CopilotLanguageServerConnection.java @@ -3,16 +3,21 @@ import java.io.IOException; import java.net.URI; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; import java.util.function.Function; import org.eclipse.jface.text.IDocument; import org.eclipse.lsp4e.LanguageServerWrapper; import org.eclipse.lsp4j.services.LanguageServer; +import com.microsoft.copilot.eclipse.core.AuthStatusManager; import com.microsoft.copilot.eclipse.core.lsp.protocol.AuthStatusResult; import com.microsoft.copilot.eclipse.core.lsp.protocol.CheckStatusParams; import com.microsoft.copilot.eclipse.core.lsp.protocol.CompletionParams; import com.microsoft.copilot.eclipse.core.lsp.protocol.CompletionResult; +import com.microsoft.copilot.eclipse.core.lsp.protocol.NullParams; +import com.microsoft.copilot.eclipse.core.lsp.protocol.SignInConfirmParams; +import com.microsoft.copilot.eclipse.core.lsp.protocol.SignInInitiateResult; /** * Language Server for Copilot agent. @@ -76,6 +81,38 @@ public CompletableFuture getCompletions(CompletionParams param return this.languageServerWrapper.execute(fn); } + /** + * Please use the {@link AuthStatusManager#signInInitiate()} method instead.

+ * Initiate the sign in process. + */ + public CompletableFuture signInInitiate() { + Function> fn = (server) -> ((CopilotLanguageServer) server) + .signInInitiate(new NullParams()); + return this.languageServerWrapper.execute(fn); + } + + /** + * Please use the {@link AuthStatusManager#signInConfirm()} method instead.

+ * Confirm the sign in process. + */ + public CompletableFuture signInConfirm(String userCode) { + Function> fn = (server) -> { + SignInConfirmParams param = new SignInConfirmParams(userCode); + return ((CopilotLanguageServer) server).signInConfirm(param); + }; + return this.languageServerWrapper.execute(fn); + } + + /** + * Please use the {@link AuthStatusManager#signOut()} method instead.

+ * Sign out from the GitHub Copilot. + */ + public CompletableFuture signOut() { + Function> fn = (server) -> ((CopilotLanguageServer) server) + .signOut(new NullParams()); + return this.languageServerWrapper.execute(fn); + } + /** * Stop the language server. */ diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/AuthStatusResult.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/AuthStatusResult.java index 37c95e28..83a637e2 100644 --- a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/AuthStatusResult.java +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/AuthStatusResult.java @@ -13,6 +13,7 @@ public class AuthStatusResult { public static final String OK = "OK"; public static final String ERROR = "Error"; + public static final String LOADING = "Loading"; public static final String WARNING = "Warning"; public static final String NOT_SIGNED_IN = "NotSignedIn"; public static final String NOT_AUTHORIZED = "NotAuthorized"; @@ -57,6 +58,10 @@ public boolean isError() { public boolean isNotAuthorized() { return NOT_AUTHORIZED.equals(this.status); } + + public boolean isLoading() { + return LOADING.equals(this.status); + } @Override public int hashCode() { diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/NullParams.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/NullParams.java new file mode 100644 index 00000000..4c6f4383 --- /dev/null +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/NullParams.java @@ -0,0 +1,8 @@ +package com.microsoft.copilot.eclipse.core.lsp.protocol; + +/** + * Null parameters. + */ +public class NullParams { + +} diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/SignInConfirmParams.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/SignInConfirmParams.java new file mode 100644 index 00000000..c7552cec --- /dev/null +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/SignInConfirmParams.java @@ -0,0 +1,56 @@ +package com.microsoft.copilot.eclipse.core.lsp.protocol; + +import java.util.Objects; + +import org.eclipse.lsp4j.jsonrpc.util.ToStringBuilder; +import org.eclipse.lsp4j.jsonrpc.validation.NonNull; + +/** + * Parameter for SignInConfirm request. + */ +public class SignInConfirmParams { + @NonNull + public String userCode; + + /** + * Create a new parameter for SignInConfirm request. + */ + public SignInConfirmParams(String userCode) { + this.userCode = userCode; + } + + public String getUserCode() { + return userCode; + } + + public void setUserCode(String userCode) { + this.userCode = userCode; + } + + @Override + public int hashCode() { + return Objects.hash(userCode); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + SignInConfirmParams other = (SignInConfirmParams) obj; + return Objects.equals(userCode, other.userCode); + } + + @Override + public String toString() { + ToStringBuilder builder = new ToStringBuilder(this); + builder.add("userCode", userCode); + return builder.toString(); + } +} diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/SignInInitiateResult.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/SignInInitiateResult.java new file mode 100644 index 00000000..c78a9150 --- /dev/null +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/SignInInitiateResult.java @@ -0,0 +1,102 @@ +package com.microsoft.copilot.eclipse.core.lsp.protocol; + +import java.util.Objects; + +import org.eclipse.lsp4j.jsonrpc.util.ToStringBuilder; + +/** + * Result for signInInitiate. + */ +public class SignInInitiateResult { + + public static final String ALREADY_SIGNED_IN = "AlreadySignedIn"; + + private String status; + private Integer expiresIn; + private Integer interval; + private String userCode; + private String verificationUri; + + /** + * Create a new SignInInitiateResult. + */ + public SignInInitiateResult() { + } + + public boolean isAlreadySignedIn() { + return ALREADY_SIGNED_IN.equals(status); + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public Integer getExpiresIn() { + return expiresIn; + } + + public void setExpiresIn(Integer expiresIn) { + this.expiresIn = expiresIn; + } + + public Integer getInterval() { + return interval; + } + + public void setInterval(Integer interval) { + this.interval = interval; + } + + public String getUserCode() { + return userCode; + } + + public void setUserCode(String userCode) { + this.userCode = userCode; + } + + public String getVerificationUri() { + return verificationUri; + } + + public void setVerificationUri(String verificationUri) { + this.verificationUri = verificationUri; + } + + @Override + public int hashCode() { + return Objects.hash(expiresIn, interval, status, userCode, verificationUri); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + SignInInitiateResult other = (SignInInitiateResult) obj; + return Objects.equals(expiresIn, other.expiresIn) && Objects.equals(interval, other.interval) + && Objects.equals(status, other.status) && Objects.equals(userCode, other.userCode) + && Objects.equals(verificationUri, other.verificationUri); + } + + @Override + public String toString() { + ToStringBuilder builder = new ToStringBuilder(this); + builder.add("status", status); + builder.add("expiresIn", expiresIn); + builder.add("interval", interval); + builder.add("userCode", userCode); + builder.add("verificationUri", verificationUri); + return builder.toString(); + } +} diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/utils/PlatformUtils.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/utils/PlatformUtils.java index 9168986e..30277143 100644 --- a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/utils/PlatformUtils.java +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/utils/PlatformUtils.java @@ -24,6 +24,22 @@ public static String getEclipseVersion() { return bundle.getVersion().toString(); } + /** + * Escapes spaces in a URL string. + */ + public static String escapeSpaceInUrl(String urlString) { + char[] chars = urlString.toCharArray(); + StringBuffer sb = new StringBuffer(chars.length); + for (int i = 0; i < chars.length; i++) { + if (chars[i] == ' ') { + sb.append("%20"); + } else { + sb.append(chars[i]); + } + } + return sb.toString(); + } + public static boolean isMac() { return Platform.getOS().equals(Platform.OS_MACOSX); } diff --git a/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/i18n/MessagesTest.java b/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/i18n/MessagesTest.java index 38a9e62b..e5314298 100644 --- a/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/i18n/MessagesTest.java +++ b/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/i18n/MessagesTest.java @@ -9,7 +9,7 @@ class MessagesTest { @Test void testMessagesInitialization() { // Ensure that the static fields are initialized - assertNotNull(Messages.INFO_signToGitHub); - assertNotNull(Messages.INFO_signOutFromGitHub); + assertNotNull(Messages.menu_signToGitHub); + assertNotNull(Messages.menu_signOutFromGitHub); } } \ No newline at end of file diff --git a/com.microsoft.copilot.eclipse.ui/META-INF/MANIFEST.MF b/com.microsoft.copilot.eclipse.ui/META-INF/MANIFEST.MF index 267e163b..be667e90 100644 --- a/com.microsoft.copilot.eclipse.ui/META-INF/MANIFEST.MF +++ b/com.microsoft.copilot.eclipse.ui/META-INF/MANIFEST.MF @@ -5,8 +5,10 @@ Bundle-SymbolicName: com.microsoft.copilot.eclipse.ui;singleton:=true Bundle-Version: 0.1.0.qualifier Export-Package: com.microsoft.copilot.eclipse.ui, com.microsoft.copilot.eclipse.ui.completion, + com.microsoft.copilot.eclipse.ui.dialogs, com.microsoft.copilot.eclipse.ui.handlers, - com.microsoft.copilot.eclipse.ui.i18n + com.microsoft.copilot.eclipse.ui.i18n, + com.microsoft.copilot.eclipse.ui.utils Bundle-Activator: com.microsoft.copilot.eclipse.ui.CopilotUi Bundle-RequiredExecutionEnvironment: JavaSE-17 Automatic-Module-Name: com.microsoft.copilot.eclipse.ui diff --git a/com.microsoft.copilot.eclipse.ui/plugin.properties b/com.microsoft.copilot.eclipse.ui/plugin.properties index f2b1b840..4aadecf3 100644 --- a/com.microsoft.copilot.eclipse.ui/plugin.properties +++ b/com.microsoft.copilot.eclipse.ui/plugin.properties @@ -1,3 +1,6 @@ command.triggerInlineSuggestions.name=Trigger Inline Suggestion command.acceptFullSuggestion.name=Accept Suggestion command.discardSuggestion.name=Discard Suggestion +command.copilotForEclipsePlugin.name=GitHub Copilot for Eclipse +command.signInToGitHub.name=Sign in to GitHub Copilot +command.signOutFromGitHub.name=Sign out from GitHub Copilot \ No newline at end of file diff --git a/com.microsoft.copilot.eclipse.ui/plugin.xml b/com.microsoft.copilot.eclipse.ui/plugin.xml index 0e341bfd..50a70a5a 100644 --- a/com.microsoft.copilot.eclipse.ui/plugin.xml +++ b/com.microsoft.copilot.eclipse.ui/plugin.xml @@ -28,26 +28,17 @@ - - - - - + id="com.microsoft.copilot.eclipse.commands.showStatusBarMenu" + name="%command.copilotForEclipsePlugin.name"> + + + + + @@ -69,6 +60,14 @@ class="com.microsoft.copilot.eclipse.ui.handlers.ShowStatusBarMenuHandler" commandId="com.microsoft.copilot.eclipse.commands.showStatusBarMenu"> + + + + diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/dialogs/SignInConfirmDialog.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/dialogs/SignInConfirmDialog.java new file mode 100644 index 00000000..998ca5f5 --- /dev/null +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/dialogs/SignInConfirmDialog.java @@ -0,0 +1,133 @@ +package com.microsoft.copilot.eclipse.ui.dialogs; + +import java.lang.reflect.InvocationTargetException; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; + +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Status; +import org.eclipse.jface.dialogs.ProgressMonitorDialog; +import org.eclipse.jface.operation.IRunnableWithProgress; +import org.eclipse.swt.widgets.Shell; + +import com.microsoft.copilot.eclipse.core.CopilotCore; +import com.microsoft.copilot.eclipse.core.lsp.protocol.AuthStatusResult; +import com.microsoft.copilot.eclipse.ui.i18n.Messages; + +/** + * Dialog for confirming sign-in to GitHub Copilot. + */ +public class SignInConfirmDialog extends ProgressMonitorDialog { + + private final String userCode; + private final long timeout; + private CompletableFuture future; + private IStatus status; + + /** + * Constructs a new SignInConfirmDialog. + * + * @param parent the parent shell + * @param userCode the user code for sign-in confirmation + * @param timeout the timeout duration in milliseconds + */ + public SignInConfirmDialog(Shell parent, String userCode, long timeout) { + super(parent); + this.userCode = userCode; + this.timeout = timeout; + this.future = null; + this.setCancelable(true); + } + + @Override + protected void configureShell(Shell shell) { + super.configureShell(shell); + shell.setText(Messages.signInDialog_title); + } + + /** + * Runs the sign-in confirmation process. + */ + public void run() { + IRunnableWithProgress task = new SignInConfirmationTask(); + try { + this.run(true, true, task); + } catch (Exception e) { + // TODO: log & send telemetry + } + } + + /** + * Gets the status of the sign-in confirmation. + * + * @return the status of the sign-in confirmation + */ + public IStatus getStatus() { + return this.status; + } + + private class SignInConfirmationTask implements IRunnableWithProgress { + @Override + public void run(IProgressMonitor monitor) throws InvocationTargetException, InterruptedException { + try { + future = CompletableFuture.supplyAsync(() -> { + try { + return CopilotCore.getPlugin().getAuthStatusManager().signInConfirm(userCode); + } catch (Exception e) { + // TODO: log & send telemetry + return null; + } + }); + + monitor.beginTask(Messages.signInConfirmDialog_progress, (int) timeout / 250); + + waitForAuthorization(monitor); + + if (future.isDone()) { + handleAuthorizationResult(); + } else { + future.cancel(true); + status = Status.error(Messages.signInConfirmDialog_progressTimeout); + } + } catch (ExecutionException | InterruptedException e) { + status = Status.error(Messages.signInConfirmDialog_progressCanceled); + } + } + + private void waitForAuthorization(IProgressMonitor monitor) throws InterruptedException { + int step = 250; + int steps = (int) timeout / step; + + for (int i = 0; i < steps; i++) { + Thread.sleep(step); + + if (monitor.isCanceled()) { + future.cancel(true); + return; + } + + if (future.isDone()) { + break; + } + + monitor.worked(1); + } + + monitor.done(); + } + + private void handleAuthorizationResult() throws ExecutionException, InterruptedException { + AuthStatusResult result = future.get(); + String errorMsg = null; + + if (result == null || !result.isSignedIn()) { + errorMsg = Messages.signInConfirmDialog_authResult_notSignedIn; + } else if (result.isNotAuthorized()) { + errorMsg = Messages.signInConfirmDialog_authResult_notAuthed; + } + + status = errorMsg != null ? Status.error(errorMsg) : Status.OK_STATUS; + } + } +} diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/dialogs/SignInDialog.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/dialogs/SignInDialog.java new file mode 100644 index 00000000..38e100b3 --- /dev/null +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/dialogs/SignInDialog.java @@ -0,0 +1,100 @@ +package com.microsoft.copilot.eclipse.ui.dialogs; + +import org.eclipse.jface.dialogs.MessageDialog; +import org.eclipse.swt.SWT; +import org.eclipse.swt.dnd.Clipboard; +import org.eclipse.swt.dnd.TextTransfer; +import org.eclipse.swt.dnd.Transfer; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Link; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.Text; + +import com.microsoft.copilot.eclipse.core.lsp.protocol.SignInInitiateResult; +import com.microsoft.copilot.eclipse.ui.i18n.Messages; +import com.microsoft.copilot.eclipse.ui.utils.SwtUtils; +import com.microsoft.copilot.eclipse.ui.utils.UiUtils; + +/** + * Dialog for signing in to GitHub Copilot. + */ +public class SignInDialog extends MessageDialog { + + private final SignInInitiateResult signInInitiateResult; + + /** + * Constructs a new SignInDialog. + * + * @param parentShell the parent shell + * @param initResult the sign-in initiation result + */ + public SignInDialog(Shell parentShell, SignInInitiateResult initResult) { + super(parentShell, Messages.signInDialog_title, null, null, MessageDialog.INFORMATION, + new String[] { Messages.signInDialog_button_cancel, Messages.signInDialog_button_copyOpen }, 1); + this.signInInitiateResult = initResult; + } + + @Override + protected Control createCustomArea(Composite parent) { + Composite composite = createComposite(parent); + createDeviceCodeSection(composite); + createWebsiteSection(composite); + return composite; + } + + private Composite createComposite(Composite parent) { + Composite composite = new Composite(parent, SWT.NONE); + composite.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, true, true)); + composite.setLayout(new GridLayout(2, false)); + return composite; + } + + private void createDeviceCodeSection(Composite composite) { + Label deviceCodeLabel = new Label(composite, SWT.NONE); + deviceCodeLabel.setText(Messages.signInDialog_info_deviceCodePrefix); + Text deviceCodeText = new Text(composite, SWT.SINGLE | SWT.READ_ONLY); + deviceCodeText.setText(this.signInInitiateResult.getUserCode()); + deviceCodeText.setCursor(composite.getDisplay().getSystemCursor(SWT.CURSOR_ARROW)); + deviceCodeText.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false)); + } + + private void createWebsiteSection(Composite composite) { + Label websiteLabel = new Label(composite, SWT.NONE); + websiteLabel.setText(Messages.signInDialog_info_gitHubWebSitePrefix); + Link websiteLink = new Link(composite, SWT.NONE); + websiteLink.setText("" + this.signInInitiateResult.getVerificationUri() + ""); + websiteLink.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + UiUtils.openLink(signInInitiateResult.getVerificationUri()); + } + }); + } + + @Override + protected Control createMessageArea(Composite composite) { + this.message = Messages.signInDialog_info_instructions; + return super.createMessageArea(composite); + } + + @Override + protected void buttonPressed(int buttonId) { + if (buttonId == 1) { + copyCodeToClipboard(); + } + super.buttonPressed(buttonId); + } + + private void copyCodeToClipboard() { + Clipboard clipboard = new Clipboard(SwtUtils.getDisplay()); + TextTransfer textTransfer = TextTransfer.getInstance(); + clipboard.setContents(new Object[] { this.signInInitiateResult.getUserCode() }, new Transfer[] { textTransfer }); + clipboard.dispose(); + } +} diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/ShowStatusBarMenuHandler.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/ShowStatusBarMenuHandler.java index d94cdace..acf67332 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/ShowStatusBarMenuHandler.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/ShowStatusBarMenuHandler.java @@ -5,12 +5,17 @@ import org.eclipse.core.commands.ExecutionException; import org.eclipse.jface.action.Action; import org.eclipse.jface.action.MenuManager; +import org.eclipse.jface.action.Separator; import org.eclipse.jface.resource.ImageDescriptor; import org.eclipse.swt.widgets.Menu; import org.eclipse.swt.widgets.Shell; import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.handlers.HandlerUtil; +import org.eclipse.ui.handlers.IHandlerService; -import com.microsoft.copilot.eclipse.ui.UiConstants; +import com.microsoft.copilot.eclipse.core.AuthStatusManager; +import com.microsoft.copilot.eclipse.core.CopilotCore; +import com.microsoft.copilot.eclipse.core.lsp.protocol.AuthStatusResult; import com.microsoft.copilot.eclipse.ui.i18n.Messages; import com.microsoft.copilot.eclipse.ui.utils.UiUtils; @@ -19,38 +24,85 @@ */ public class ShowStatusBarMenuHandler extends AbstractHandler { - /** - * Render the status bar menu based on the logged-in state. - */ + private IHandlerService handlerService; + private ImageDescriptor icon; + private AuthStatusManager authStatusManager; + @Override public Object execute(ExecutionEvent event) throws ExecutionException { - - Shell shell = PlatformUI.getWorkbench().getDisplay().getActiveShell(); + handlerService = HandlerUtil.getActiveWorkbenchWindow(event).getService(IHandlerService.class); + authStatusManager = CopilotCore.getPlugin().getAuthStatusManager(); + icon = ImageDescriptor.createFromURL(UiUtils.class.getResource("/icons/copilot.png")); MenuManager menuManager = new MenuManager(); - ImageDescriptor icon = UiUtils.resizeIcon("/icons/copilot.png", UiConstants.TOOLBAR_ICON_WIDTH_IN_PIEXL, - UiConstants.TOOLBAR_ICON_HEIGHT_IN_PIEXL); - - // TODO: Add GitHub sign-in states to the menu - Action signInAction = new Action(Messages.INFO_signToGitHub, icon) { - @Override - public void run() { - // Handle sign-in action - } - }; - - // TODO: Add GitHub sign-out states to the menu - Action signOutAction = new Action(Messages.INFO_signOutFromGitHub, icon) { - @Override - public void run() { - // Handle sign-out action - } - }; - - menuManager.add(signInAction); - menuManager.add(signOutAction); + addStatusAction(menuManager); + + if (!authStatusManager.getAuthStatusResult().isLoading()) { + menuManager.add(new Separator()); + addSignInOrSignOutAction(menuManager); + } + Shell shell = PlatformUI.getWorkbench().getDisplay().getActiveShell(); Menu menu = menuManager.createContextMenu(shell); menu.setVisible(true); return null; } + + private void addStatusAction(MenuManager menuManager) { + String signInStatus = getSignInStatusBasedOnAuthResult(authStatusManager.getAuthStatusResult()); + String signInStatusTitle = Messages.menu_signInStatus + ": " + signInStatus; + + MenuActionFactory.createMenuAction(menuManager, signInStatusTitle, handlerService, signInStatus, false); + } + + private String getSignInStatusBasedOnAuthResult(AuthStatusResult authStatusResult) { + switch (authStatusResult.getStatus()) { + case AuthStatusResult.OK: + return Messages.menu_signInStatus_ready; + case AuthStatusResult.ERROR: + return Messages.menu_signInStatus_unknownError; + case AuthStatusResult.LOADING: + return Messages.menu_signInStatus_loading; + case AuthStatusResult.NOT_SIGNED_IN: + return Messages.menu_signInStatus_notSignedInToGitHub; + case AuthStatusResult.WARNING: + return Messages.menu_signInStatus_agentWarning; + case AuthStatusResult.NOT_AUTHORIZED: + return Messages.menu_signInStatus_notAuthorized; + default: + return Messages.menu_signInStatus_loading; + } + } + + private void addSignInOrSignOutAction(MenuManager menuManager) { + if (authStatusManager.getAuthStatusResult().isSignedIn()) { + MenuActionFactory.createMenuAction(menuManager, Messages.menu_signOutFromGitHub, icon, handlerService, + "com.microsoft.copilot.eclipse.commands.signOut", true); + } else { + MenuActionFactory.createMenuAction(menuManager, Messages.menu_signToGitHub, icon, handlerService, + "com.microsoft.copilot.eclipse.commands.signIn", true); + } + } + + private static class MenuActionFactory { + public static void createMenuAction(MenuManager menuManager, String actionName, ImageDescriptor icon, + IHandlerService handlerService, String commandId, boolean enabled) { + Action action = new Action(actionName, icon) { + @Override + public void run() { + try { + handlerService.executeCommand(commandId, null); + } catch (Exception e) { + // TODO: log & send telemetry + } + } + }; + action.setEnabled(enabled); + menuManager.add(action); + } + + public static void createMenuAction(MenuManager menuManager, String text, IHandlerService handlerService, + String commandId, boolean enabled) { + createMenuAction(menuManager, text, null, handlerService, commandId, enabled); + } + } } diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/SignInHandler.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/SignInHandler.java new file mode 100644 index 00000000..0c85a367 --- /dev/null +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/SignInHandler.java @@ -0,0 +1,116 @@ +package com.microsoft.copilot.eclipse.ui.handlers; + +import java.util.concurrent.atomic.AtomicReference; + +import org.apache.commons.lang3.StringUtils; +import org.eclipse.core.commands.AbstractHandler; +import org.eclipse.core.commands.ExecutionEvent; +import org.eclipse.core.commands.ExecutionException; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.jface.dialogs.MessageDialog; +import org.eclipse.swt.widgets.Shell; + +import com.microsoft.copilot.eclipse.core.AuthStatusManager; +import com.microsoft.copilot.eclipse.core.CopilotCore; +import com.microsoft.copilot.eclipse.core.lsp.CopilotLanguageServerConnection; +import com.microsoft.copilot.eclipse.core.lsp.protocol.SignInInitiateResult; +import com.microsoft.copilot.eclipse.ui.dialogs.SignInConfirmDialog; +import com.microsoft.copilot.eclipse.ui.dialogs.SignInDialog; +import com.microsoft.copilot.eclipse.ui.i18n.Messages; +import com.microsoft.copilot.eclipse.ui.utils.SwtUtils; +import com.microsoft.copilot.eclipse.ui.utils.UiUtils; + +/** + * Handler for signing to GitHub Copilot. + */ +public class SignInHandler extends AbstractHandler { + + private static final long SIGNIN_TIMEOUT_MILLIS = 180000L; + + private CopilotLanguageServerConnection languageServer; + private AuthStatusManager authStatusManager; + + /** + * Initialize the Copilot Language Server for the SignInHandler. + */ + public SignInHandler() { + this.languageServer = CopilotCore.getPlugin().getCopilotLanguageServer(); + this.authStatusManager = CopilotCore.getPlugin().getAuthStatusManager(); + } + + @Override + public Object execute(ExecutionEvent event) throws ExecutionException { + Shell shell = SwtUtils.getShellFromEvent(event); + + try { + SignInInitiateResult result = initiateSignIn(); + if (result.isAlreadySignedIn()) { + showAlreadySignedInMessage(shell); + } else { + handleSignIn(shell, result); + } + } catch (Exception e) { + handleSignInException(shell, e); + // TODO log & send telemetry + } + + return null; + } + + private SignInInitiateResult initiateSignIn() throws Exception { + return this.authStatusManager.signInInitiate(); + } + + private void showAlreadySignedInMessage(Shell shell) { + MessageDialog.openInformation(shell, Messages.signInHandler_msgDialog_title, + Messages.signInHandler_msgDialog_alreadySignedIn); + } + + private void handleSignIn(Shell shell, SignInInitiateResult result) { + AtomicReference signInInitiateResultHolder = new AtomicReference<>(result); + SwtUtils.invokeOnDisplayThread(() -> { + SignInDialog signInDialog = new SignInDialog(shell, signInInitiateResultHolder.get()); + int btnId = signInDialog.open(); + if (btnId > 0) { + UiUtils.openLink(signInInitiateResultHolder.get().getVerificationUri()); + SignInConfirmDialog signInConfirmDialog = new SignInConfirmDialog(shell, + signInInitiateResultHolder.get().getUserCode(), SIGNIN_TIMEOUT_MILLIS); + signInConfirmDialog.run(); + handleSignInConfirmation(shell, signInConfirmDialog); + } + }); + } + + private void handleSignInConfirmation(Shell shell, SignInConfirmDialog signInConfirmDialog) { + IStatus status = signInConfirmDialog.getStatus(); + if (status != null && status.isOK()) { + showSignInSuccessMessage(shell); + } else { + showSignInFailMessage(shell, status); + } + } + + private void showSignInSuccessMessage(Shell shell) { + MessageDialog.openInformation(shell, Messages.signInHandler_msgDialog_gitHubCopilot, + Messages.signInHandler_msgDialog_signInSuccess); + } + + private void showSignInFailMessage(Shell shell, IStatus status) { + String msg = Messages.signInHandler_msgDialog_signInFailed; + if (status != null && StringUtils.isNotBlank(status.getMessage())) { + msg += ": " + status.getMessage(); + } + msg += ". "; + MessageDialog.openInformation(shell, Messages.signInHandler_msgDialog_gitHubCopilot, + msg + Messages.signInHandler_msgDialog_signInFailedTryAgain); + } + + private void handleSignInException(Shell shell, Exception e) { + String msg = Messages.signInHandler_msgDialog_signInFailed; + if (StringUtils.isNotBlank(e.getMessage())) { + msg += " " + e.getMessage(); + // TODO log & send telemetry + } + MessageDialog.openError(shell, Messages.signInHandler_msgDialog_signInFailedFailure, msg); + } +} diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/SignOutHandler.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/SignOutHandler.java new file mode 100644 index 00000000..0dd913d7 --- /dev/null +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/SignOutHandler.java @@ -0,0 +1,64 @@ +package com.microsoft.copilot.eclipse.ui.handlers; + +import org.apache.commons.lang3.StringUtils; +import org.eclipse.core.commands.AbstractHandler; +import org.eclipse.core.commands.ExecutionEvent; +import org.eclipse.core.commands.ExecutionException; +import org.eclipse.jface.dialogs.MessageDialog; +import org.eclipse.swt.widgets.Shell; + +import com.microsoft.copilot.eclipse.core.AuthStatusManager; +import com.microsoft.copilot.eclipse.core.CopilotCore; +import com.microsoft.copilot.eclipse.core.lsp.CopilotLanguageServerConnection; +import com.microsoft.copilot.eclipse.core.lsp.protocol.AuthStatusResult; +import com.microsoft.copilot.eclipse.ui.i18n.Messages; +import com.microsoft.copilot.eclipse.ui.utils.SwtUtils; + +/** + * Handler for signing out from GitHub Copilot. + */ +public class SignOutHandler extends AbstractHandler { + + private CopilotLanguageServerConnection languageServer; + private AuthStatusManager authStatusManager; + + /** + * Initialize the Copilot Language Server and Auth Status Manager for the SignOutHandler. + */ + public SignOutHandler() { + this.languageServer = CopilotCore.getPlugin().getCopilotLanguageServer(); + this.authStatusManager = CopilotCore.getPlugin().getAuthStatusManager(); + } + + @Override + public Object execute(ExecutionEvent event) throws ExecutionException { + Shell shell = SwtUtils.getShellFromEvent(event); + try { + AuthStatusResult result = this.languageServer.signOut().get(); + if (!result.isSignedIn()) { + showSignOutMessage(shell); + authStatusManager.checkStatus(); + } + } catch (Exception e) { + handleSignOutException(shell, e); + // TODO: log & send telemetry + } + + return null; + } + + private void handleSignOutException(Shell shell, Exception e) { + String msg = Messages.signOutHandler_msgDialog_signOutFailed; + if (StringUtils.isNotBlank(e.getMessage())) { + msg += " " + e.getMessage(); + // TODO: log & send telemetry + } + MessageDialog.openError(shell, Messages.signOutHandler_msgDialog_signOutFailedFailure, msg); + } + + private void showSignOutMessage(Shell shell) { + MessageDialog.openInformation(shell, Messages.signOutHandler_msgDialog_gitHubCopilot, + Messages.signOutHandler_msgDialog_signOutSuccess); + } + +} diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/i18n/Messages.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/i18n/Messages.java index 94e0afaa..d17b897e 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/i18n/Messages.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/i18n/Messages.java @@ -7,8 +7,38 @@ */ public final class Messages extends NLS { private static final String BUNDLE_NAME = "com.microsoft.copilot.eclipse.ui.i18n.messages"; //$NON-NLS-1$ - public static String INFO_signToGitHub; - public static String INFO_signOutFromGitHub; + public static String menu_signInStatus; + public static String menu_signInStatus_ready; + public static String menu_signInStatus_loading; + public static String menu_signInStatus_notSignedInToGitHub; + public static String menu_signInStatus_unknownError; + public static String menu_signInStatus_notAuthorized; + public static String menu_signInStatus_agentWarning; + public static String menu_signToGitHub; + public static String menu_signOutFromGitHub; + public static String signInDialog_title; + public static String signInDialog_button_cancel; + public static String signInDialog_button_copyOpen; + public static String signInDialog_info_instructions; + public static String signInDialog_info_deviceCodePrefix; + public static String signInDialog_info_gitHubWebSitePrefix; + public static String signInConfirmDialog_progress; + public static String signInConfirmDialog_progressSuffix; + public static String signInConfirmDialog_progressTimeout; + public static String signInConfirmDialog_progressCanceled; + public static String signInConfirmDialog_authResult_notSignedIn; + public static String signInConfirmDialog_authResult_notAuthed; + public static String signInHandler_msgDialog_gitHubCopilot; + public static String signInHandler_msgDialog_title; + public static String signInHandler_msgDialog_alreadySignedIn; + public static String signInHandler_msgDialog_signInSuccess; + public static String signInHandler_msgDialog_signInFailed; + public static String signInHandler_msgDialog_signInFailedTryAgain; + public static String signInHandler_msgDialog_signInFailedFailure; + public static String signOutHandler_msgDialog_gitHubCopilot; + public static String signOutHandler_msgDialog_signOutSuccess; + public static String signOutHandler_msgDialog_signOutFailed; + public static String signOutHandler_msgDialog_signOutFailedFailure; static { // initialize resource bundle diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/i18n/messages.properties b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/i18n/messages.properties index ae3f5feb..50672d46 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/i18n/messages.properties +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/i18n/messages.properties @@ -1,2 +1,32 @@ -INFO_signToGitHub=Sign In to GitHub -INFO_signOutFromGitHub=Sign Out from GitHub +menu_signInStatus=Status +menu_signInStatus_ready=Ready +menu_signInStatus_loading=Loading +menu_signInStatus_notSignedInToGitHub=Not signed in to GitHub +menu_signInStatus_unknownError=Unknown error +menu_signInStatus_notAuthorized=No access to GitHub Copilot +menu_signInStatus_agentWarning=Copilot is encountering temporary issues +menu_signToGitHub=Sign In to GitHub +menu_signOutFromGitHub=Sign Out from GitHub + +signInDialog_title=Sign In to GitHub +signInDialog_button_cancel=Cancel +signInDialog_button_copyOpen=Copy Code and Open +signInDialog_info_instructions=GitHub Copilot uses a GitHub account. Please enter the following code on the GitHub website to authorize your GitHub account with GitHub Copilot. +signInDialog_info_deviceCodePrefix=Device code: +signInDialog_info_gitHubWebSitePrefix=GitHub website: +signInConfirmDialog_progress=Waiting for GitHub Copilot authorization... +signInConfirmDialog_progressTimeout=Authorization request failed: Process timed out. +signInConfirmDialog_progressCanceled=Authorization request failed: process aborted. +signInConfirmDialog_authResult_notSignedIn=Authorization request failed: Not signed in. +signInConfirmDialog_authResult_notAuthed=Authorization request failed: Your subscription may be expired. +signInHandler_msgDialog_gitHubCopilot=GitHub Copilot +signInHandler_msgDialog_title=Sign In to GitHub +signInHandler_msgDialog_alreadySignedIn=User already signed in. +signInHandler_msgDialog_signInSuccess=You have successfully signed in and authorized GitHub Copilot access to your GitHub account. +signInHandler_msgDialog_signInFailed=Unable to sign in to GitHub Copilot at this time +signInHandler_msgDialog_signInFailedTryAgain= Please try again to resume use of GitHub Copilot features. +signInHandler_msgDialog_signInFailedFailure=Copilot Sign In Failure +signOutHandler_msgDialog_gitHubCopilot=GitHub Copilot +signOutHandler_msgDialog_signOutSuccess=You have successfully signed out from Copilot. +signOutHandler_msgDialog_signOutFailed=Unable to sign out to GitHub Copilot at this time +signOutHandler_msgDialog_signOutFailedFailure=Copilot Sign Out Failure \ No newline at end of file diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/utils/SwtUtils.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/utils/SwtUtils.java index b745f8ed..8bea0b49 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/utils/SwtUtils.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/utils/SwtUtils.java @@ -3,13 +3,17 @@ import java.util.Objects; import java.util.concurrent.atomic.AtomicReference; +import org.eclipse.core.commands.ExecutionEvent; +import org.eclipse.core.commands.ExecutionException; import org.eclipse.jdt.annotation.Nullable; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Shell; import org.eclipse.ui.IEditorPart; import org.eclipse.ui.IWorkbench; import org.eclipse.ui.IWorkbenchWindow; import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.handlers.HandlerUtil; /** * Utilities for SWT. * @@ -68,4 +72,26 @@ public static IEditorPart getActiveEditorPart() { }); return ref.get(); } + + /** + * This method retrieves the active workbench window from the event and then gets the shell associated with that + * window. It is more specific to the Eclipse framework and is typically used in handlers for commands or actions + * within the Eclipse environment. + + * @throws ExecutionException if the active workbench window cannot be retrieved from the event. + */ + public static Shell getShellFromEvent(ExecutionEvent event) throws ExecutionException { + return HandlerUtil.getActiveWorkbenchWindowChecked(event).getShell(); + } + + /** + * Get current display. + */ + public static Display getDisplay() { + Display display = Display.getCurrent(); + if (display == null) { + display = Display.getDefault(); + } + return display; + } } diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/utils/UiUtils.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/utils/UiUtils.java index 69d91632..ad306c53 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/utils/UiUtils.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/utils/UiUtils.java @@ -1,7 +1,10 @@ package com.microsoft.copilot.eclipse.ui.utils; +import java.net.URI; import java.util.concurrent.atomic.AtomicInteger; +import org.eclipse.core.resources.IFile; +import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jface.resource.ImageDescriptor; import org.eclipse.jface.text.ITextViewer; import org.eclipse.swt.custom.StyledText; @@ -9,6 +12,14 @@ import org.eclipse.swt.graphics.ImageData; import org.eclipse.swt.graphics.ImageLoader; import org.eclipse.swt.widgets.Display; +import org.eclipse.ui.IEditorInput; +import org.eclipse.ui.IFileEditorInput; +import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.browser.IWebBrowser; +import org.eclipse.ui.browser.IWorkbenchBrowserSupport; +import org.eclipse.ui.texteditor.ITextEditor; + +import com.microsoft.copilot.eclipse.core.utils.PlatformUtils; /** * Utilities for Eclipse UI. @@ -19,6 +30,36 @@ private UiUtils() { // prevent instantiation } + /** + * Gets the URI of the file opened in the given text editor. + */ + @Nullable + public static URI getUriFromTextEditor(ITextEditor editor) { + IEditorInput input = editor.getEditorInput(); + if (input instanceof IFileEditorInput fileInput) { + IFile file = fileInput.getFile(); + return file.getLocationURI(); + } + + return null; + } + + /** + * Open the given link in a new browser page. + */ + public static boolean openLink(String link) { + String encodedUrl = PlatformUtils.escapeSpaceInUrl(link); + IWorkbenchBrowserSupport browserSupport = PlatformUI.getWorkbench().getBrowserSupport(); + try { + IWebBrowser browser = browserSupport.createBrowser(IWorkbenchBrowserSupport.AS_EXTERNAL, null, null, null); + browser.openURL(new URI(encodedUrl).toURL()); + } catch (Exception e) { + // TODO: log & send telemetry + return false; + } + return true; + } + /** * Resizes the icon at the given path to the given width and height. Icon size is 16x16 by default, which is the * recommended size for toolbar icons. For more details: https://eclipse-platform.github.io/ui-best-practices/#toolbar @@ -46,5 +87,4 @@ public static int getCaretOffset(ITextViewer textViewer) { }, styledText); return ref.get(); } - } From e69036d3f3206e2d726548b52f5b4fc132830b53 Mon Sep 17 00:00:00 2001 From: Sheng Chen Date: Wed, 18 Dec 2024 16:54:17 +0800 Subject: [PATCH 023/690] feat - Notify the completion result (#31) * Three notification will be sent to CLS during completion: notifyShown, notifyAccepted & notifyRejected. --- .../completion/CompletionCollectionTests.java | 8 +++ .../core/completion/CompletionCollection.java | 6 +- .../core/lsp/CopilotLanguageServer.java | 22 +++++- .../lsp/CopilotLanguageServerConnection.java | 40 +++++++++-- .../lsp/protocol/NotifyAcceptedParams.java | 70 +++++++++++++++++++ .../lsp/protocol/NotifyRejectedParams.java | 59 ++++++++++++++++ .../core/lsp/protocol/NotifyShownParams.java | 58 +++++++++++++++ .../AcceptFullSuggestionHandlerTests.java | 40 +++++++++++ .../DiscardSuggestionHandlerTests.java | 39 +++++++++++ .../ui/completion/CompletionHandler.java | 5 ++ .../ui/completion/CompletionManager.java | 21 ++++++ .../handlers/AcceptFullSuggestionHandler.java | 15 ++++ .../eclipse/ui/handlers/CopilotHandler.java | 6 ++ .../ui/handlers/DiscardSuggestionHandler.java | 18 +++++ 14 files changed, 401 insertions(+), 6 deletions(-) create mode 100644 com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/NotifyAcceptedParams.java create mode 100644 com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/NotifyRejectedParams.java create mode 100644 com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/NotifyShownParams.java diff --git a/com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/completion/CompletionCollectionTests.java b/com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/completion/CompletionCollectionTests.java index 92b99a01..ca1f295e 100644 --- a/com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/completion/CompletionCollectionTests.java +++ b/com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/completion/CompletionCollectionTests.java @@ -60,4 +60,12 @@ void getNumberOfLinesReturnsCorrectNumberOfLines() { assertEquals(3, collection.getNumberOfLines()); } + @Test + void testGetUuids() { + List completions = List.of(new CompletionItem("uuid1", "test", null, "displayText1", null, 0), + new CompletionItem("uuid2", "test", null, "displayText1", null, 0)); + CompletionCollection collection = new CompletionCollection(completions, "uri"); + assertEquals(List.of("uuid1", "uuid2"), collection.getUuids()); + } + } diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/completion/CompletionCollection.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/completion/CompletionCollection.java index 68ff19e0..e3a39533 100644 --- a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/completion/CompletionCollection.java +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/completion/CompletionCollection.java @@ -93,10 +93,14 @@ public int getNumberOfLines() { return this.getText().split("\n").length; } + public List getUuids() { + return this.completions.stream().map(CompletionItem::getUuid).toList(); + } + /** * Get the current active completion item. */ - CompletionItem getCurrentItem() { + public CompletionItem getCurrentItem() { return this.completions.get(index); } diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/CopilotLanguageServer.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/CopilotLanguageServer.java index 1b214768..5368266c 100644 --- a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/CopilotLanguageServer.java +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/CopilotLanguageServer.java @@ -9,6 +9,9 @@ import com.microsoft.copilot.eclipse.core.lsp.protocol.CheckStatusParams; import com.microsoft.copilot.eclipse.core.lsp.protocol.CompletionParams; import com.microsoft.copilot.eclipse.core.lsp.protocol.CompletionResult; +import com.microsoft.copilot.eclipse.core.lsp.protocol.NotifyAcceptedParams; +import com.microsoft.copilot.eclipse.core.lsp.protocol.NotifyRejectedParams; +import com.microsoft.copilot.eclipse.core.lsp.protocol.NotifyShownParams; import com.microsoft.copilot.eclipse.core.lsp.protocol.NullParams; import com.microsoft.copilot.eclipse.core.lsp.protocol.SignInConfirmParams; import com.microsoft.copilot.eclipse.core.lsp.protocol.SignInInitiateResult; @@ -35,7 +38,7 @@ public interface CopilotLanguageServer extends LanguageServer { */ @JsonRequest CompletableFuture signInInitiate(NullParams param); - + /** * Confirm the sign in process. */ @@ -48,4 +51,21 @@ public interface CopilotLanguageServer extends LanguageServer { @JsonRequest CompletableFuture signOut(NullParams params); + /** + * Notify the language server that the completion was shown. + */ + @JsonRequest + CompletableFuture notifyShown(NotifyShownParams params); + + /** + * Notify the language server that the completion was accepted. + */ + @JsonRequest + CompletableFuture notifyAccepted(NotifyAcceptedParams params); + + /** + * Notify the language server that the completion was rejected. + */ + @JsonRequest + CompletableFuture notifyRejected(NotifyRejectedParams params); } diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/CopilotLanguageServerConnection.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/CopilotLanguageServerConnection.java index 47c46be1..0a1e6f02 100644 --- a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/CopilotLanguageServerConnection.java +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/CopilotLanguageServerConnection.java @@ -3,7 +3,6 @@ import java.io.IOException; import java.net.URI; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.TimeUnit; import java.util.function.Function; import org.eclipse.jface.text.IDocument; @@ -15,6 +14,9 @@ import com.microsoft.copilot.eclipse.core.lsp.protocol.CheckStatusParams; import com.microsoft.copilot.eclipse.core.lsp.protocol.CompletionParams; import com.microsoft.copilot.eclipse.core.lsp.protocol.CompletionResult; +import com.microsoft.copilot.eclipse.core.lsp.protocol.NotifyAcceptedParams; +import com.microsoft.copilot.eclipse.core.lsp.protocol.NotifyRejectedParams; +import com.microsoft.copilot.eclipse.core.lsp.protocol.NotifyShownParams; import com.microsoft.copilot.eclipse.core.lsp.protocol.NullParams; import com.microsoft.copilot.eclipse.core.lsp.protocol.SignInConfirmParams; import com.microsoft.copilot.eclipse.core.lsp.protocol.SignInInitiateResult; @@ -82,7 +84,8 @@ public CompletableFuture getCompletions(CompletionParams param } /** - * Please use the {@link AuthStatusManager#signInInitiate()} method instead.

+ * Please use the {@link AuthStatusManager#signInInitiate()} method instead. + *

* Initiate the sign in process. */ public CompletableFuture signInInitiate() { @@ -92,7 +95,8 @@ public CompletableFuture signInInitiate() { } /** - * Please use the {@link AuthStatusManager#signInConfirm()} method instead.

+ * Please use the {@link AuthStatusManager#signInConfirm()} method instead. + *

* Confirm the sign in process. */ public CompletableFuture signInConfirm(String userCode) { @@ -104,7 +108,8 @@ public CompletableFuture signInConfirm(String userCode) { } /** - * Please use the {@link AuthStatusManager#signOut()} method instead.

+ * Please use the {@link AuthStatusManager#signOut()} method instead. + *

* Sign out from the GitHub Copilot. */ public CompletableFuture signOut() { @@ -113,6 +118,33 @@ public CompletableFuture signOut() { return this.languageServerWrapper.execute(fn); } + /** + * Notify the language server that the completion was shown. + */ + public CompletableFuture notifyShown(NotifyShownParams params) { + Function> fn = server -> ((CopilotLanguageServer) server) + .notifyShown(params); + return this.languageServerWrapper.execute(fn); + } + + /** + * Notify the language server that the completion was accepted. + */ + public CompletableFuture notifyAccepted(NotifyAcceptedParams params) { + Function> fn = server -> ((CopilotLanguageServer) server) + .notifyAccepted(params); + return this.languageServerWrapper.execute(fn); + } + + /** + * Notify the language server that the completion was rejected. + */ + public CompletableFuture notifyRejected(NotifyRejectedParams params) { + Function> fn = server -> ((CopilotLanguageServer) server) + .notifyRejected(params); + return this.languageServerWrapper.execute(fn); + } + /** * Stop the language server. */ diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/NotifyAcceptedParams.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/NotifyAcceptedParams.java new file mode 100644 index 00000000..b6f77035 --- /dev/null +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/NotifyAcceptedParams.java @@ -0,0 +1,70 @@ +package com.microsoft.copilot.eclipse.core.lsp.protocol; + +import java.util.Objects; + +import org.eclipse.lsp4j.jsonrpc.util.ToStringBuilder; +import org.eclipse.lsp4j.jsonrpc.validation.NonNull; + +/** + * Parameter used for the notify a completion acceptance. + */ +public class NotifyAcceptedParams { + + @NonNull + private String uuid; + + private int acceptedLength; + + /** + * Create a new NotifyAcceptedParams. + */ + public NotifyAcceptedParams(String uuid) { + super(); + this.uuid = uuid; + } + + public String getUuid() { + return uuid; + } + + public void setUuid(String uuid) { + this.uuid = uuid; + } + + public int getAcceptedLength() { + return acceptedLength; + } + + public void setAcceptedLength(int acceptedLength) { + this.acceptedLength = acceptedLength; + } + + @Override + public int hashCode() { + return Objects.hash(acceptedLength, uuid); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + NotifyAcceptedParams other = (NotifyAcceptedParams) obj; + return acceptedLength == other.acceptedLength && Objects.equals(uuid, other.uuid); + } + + @Override + public String toString() { + ToStringBuilder builder = new ToStringBuilder(this); + builder.add("uuid", uuid); + builder.add("acceptedLength", acceptedLength); + return builder.toString(); + } + +} diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/NotifyRejectedParams.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/NotifyRejectedParams.java new file mode 100644 index 00000000..08146950 --- /dev/null +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/NotifyRejectedParams.java @@ -0,0 +1,59 @@ +package com.microsoft.copilot.eclipse.core.lsp.protocol; + +import java.util.List; +import java.util.Objects; + +import org.eclipse.lsp4j.jsonrpc.util.ToStringBuilder; +import org.eclipse.lsp4j.jsonrpc.validation.NonNull; + +/** + * Parameter used for the notify a completion rejection. + */ +public class NotifyRejectedParams { + + @NonNull + private List uuids; + + /** + * Create a new NotifyRejectedParams. + */ + public NotifyRejectedParams(List uuids) { + this.uuids = uuids; + } + + public List getUuids() { + return uuids; + } + + public void setUuids(List uuids) { + this.uuids = uuids; + } + + @Override + public int hashCode() { + return Objects.hash(uuids); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + NotifyRejectedParams other = (NotifyRejectedParams) obj; + return Objects.equals(uuids, other.uuids); + } + + @Override + public String toString() { + ToStringBuilder builder = new ToStringBuilder(this); + builder.add("uuids", uuids); + return builder.toString(); + } + +} diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/NotifyShownParams.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/NotifyShownParams.java new file mode 100644 index 00000000..37d8e7bc --- /dev/null +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/NotifyShownParams.java @@ -0,0 +1,58 @@ +package com.microsoft.copilot.eclipse.core.lsp.protocol; + +import java.util.Objects; + +import org.eclipse.lsp4j.jsonrpc.util.ToStringBuilder; +import org.eclipse.lsp4j.jsonrpc.validation.NonNull; + +/** + * * Parameters for the notifyShown request. + */ +public class NotifyShownParams { + + @NonNull + private String uuid; + + /** + * Creates a new NotifyShownParams. + */ + public NotifyShownParams(String uuid) { + this.uuid = uuid; + } + + public String getUuid() { + return uuid; + } + + public void setUuid(String uuid) { + this.uuid = uuid; + } + + @Override + public int hashCode() { + return Objects.hash(uuid); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + NotifyShownParams other = (NotifyShownParams) obj; + return Objects.equals(uuid, other.uuid); + } + + @Override + public String toString() { + ToStringBuilder builder = new ToStringBuilder(this); + builder.add("uuid", uuid); + return builder.toString(); + } + +} diff --git a/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/handler/AcceptFullSuggestionHandlerTests.java b/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/handler/AcceptFullSuggestionHandlerTests.java index eb57d598..ebf06b18 100644 --- a/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/handler/AcceptFullSuggestionHandlerTests.java +++ b/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/handler/AcceptFullSuggestionHandlerTests.java @@ -1,15 +1,25 @@ package com.microsoft.copilot.eclipse.ui.handler; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import java.util.List; + +import org.eclipse.core.commands.ExecutionException; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.MockedStatic; import org.mockito.junit.jupiter.MockitoExtension; +import com.microsoft.copilot.eclipse.core.CopilotCore; +import com.microsoft.copilot.eclipse.core.completion.CompletionCollection; +import com.microsoft.copilot.eclipse.core.lsp.CopilotLanguageServerConnection; +import com.microsoft.copilot.eclipse.core.lsp.protocol.CompletionItem; import com.microsoft.copilot.eclipse.ui.CopilotUi; import com.microsoft.copilot.eclipse.ui.completion.CompletionHandler; import com.microsoft.copilot.eclipse.ui.completion.EditorsManager; @@ -35,4 +45,34 @@ void testIsNotEnabledWhenNoCompletionIsAvailable() { } } + @Test + void testAcceptionNotifiedWhenCompletionIsAccepted() throws ExecutionException { + CopilotLanguageServerConnection mockedConnection = mock(CopilotLanguageServerConnection.class); + when(mockedConnection.notifyAccepted(any())).thenReturn(null); + CopilotCore mockedCore = mock(CopilotCore.class); + when(mockedCore.getCopilotLanguageServer()).thenReturn(mockedConnection); + + CompletionCollection completions = new CompletionCollection( + List.of(new CompletionItem("uuid", "text", null, "displayText", null, 0)), "uri"); + CompletionHandler mockedHandler = mock(CompletionHandler.class); + doNothing().when(mockedHandler).acceptFullSuggestion(); + when(mockedHandler.getCompletions()).thenReturn(completions); + EditorsManager mockedManager = mock(EditorsManager.class); + when(mockedManager.getActiveCompletionHandler()).thenReturn(mockedHandler); + CopilotUi mockedUi = mock(CopilotUi.class); + when(mockedUi.getEditorsManager()).thenReturn(mockedManager); + + AcceptFullSuggestionHandler handler = new AcceptFullSuggestionHandler(); + + try (MockedStatic mockedStatic = mockStatic(CopilotUi.class); + MockedStatic mockedStaticCore = mockStatic(CopilotCore.class)) { + mockedStatic.when(CopilotUi::getPlugin).thenReturn(mockedUi); + mockedStaticCore.when(CopilotCore::getPlugin).thenReturn(mockedCore); + + handler.execute(null); + + verify(mockedConnection).notifyAccepted(any()); + } + } + } diff --git a/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/handler/DiscardSuggestionHandlerTests.java b/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/handler/DiscardSuggestionHandlerTests.java index 1d9d8be3..97061c0c 100644 --- a/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/handler/DiscardSuggestionHandlerTests.java +++ b/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/handler/DiscardSuggestionHandlerTests.java @@ -1,15 +1,24 @@ package com.microsoft.copilot.eclipse.ui.handler; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import java.util.List; + +import org.eclipse.core.commands.ExecutionException; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.MockedStatic; import org.mockito.junit.jupiter.MockitoExtension; +import com.microsoft.copilot.eclipse.core.CopilotCore; +import com.microsoft.copilot.eclipse.core.completion.CompletionCollection; +import com.microsoft.copilot.eclipse.core.lsp.CopilotLanguageServerConnection; import com.microsoft.copilot.eclipse.ui.CopilotUi; import com.microsoft.copilot.eclipse.ui.completion.CompletionHandler; import com.microsoft.copilot.eclipse.ui.completion.EditorsManager; @@ -35,4 +44,34 @@ void testIsNotEnabledWhenNoCompletionIsAvailable() { } } + @Test + void testRejectionNotifiedWhenCompletionIsDiscarded() throws ExecutionException { + CopilotLanguageServerConnection mockedConnection = mock(CopilotLanguageServerConnection.class); + when(mockedConnection.notifyRejected(any())).thenReturn(null); + CopilotCore mockedCore = mock(CopilotCore.class); + when(mockedCore.getCopilotLanguageServer()).thenReturn(mockedConnection); + + CompletionCollection completions = mock(CompletionCollection.class); + when(completions.getUuids()).thenReturn(List.of("uuid")); + CompletionHandler mockedHandler = mock(CompletionHandler.class); + doNothing().when(mockedHandler).clearCompletionRendering(); + when(mockedHandler.getCompletions()).thenReturn(completions); + EditorsManager mockedManager = mock(EditorsManager.class); + when(mockedManager.getActiveCompletionHandler()).thenReturn(mockedHandler); + CopilotUi mockedUi = mock(CopilotUi.class); + when(mockedUi.getEditorsManager()).thenReturn(mockedManager); + + DiscardSuggestionHandler handler = new DiscardSuggestionHandler(); + + try (MockedStatic mockedStatic = mockStatic(CopilotUi.class); + MockedStatic mockedStaticCore = mockStatic(CopilotCore.class)) { + mockedStatic.when(CopilotUi::getPlugin).thenReturn(mockedUi); + mockedStaticCore.when(CopilotCore::getPlugin).thenReturn(mockedCore); + + handler.execute(null); + + verify(mockedConnection).notifyRejected(any()); + } + } + } diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionHandler.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionHandler.java index 825a3c51..367dff8b 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionHandler.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionHandler.java @@ -17,6 +17,7 @@ import org.eclipse.swt.custom.CaretListener; import org.eclipse.ui.texteditor.ITextEditor; +import com.microsoft.copilot.eclipse.core.completion.CompletionCollection; import com.microsoft.copilot.eclipse.core.completion.CompletionProvider; import com.microsoft.copilot.eclipse.core.lsp.CopilotLanguageServerConnection; import com.microsoft.copilot.eclipse.ui.utils.SwtUtils; @@ -130,6 +131,10 @@ public void clearCompletionRendering() { this.completionManager.clearGhostText(this.triggerPosition); } + public CompletionCollection getCompletions() { + return this.completionManager.getCompletions(); + } + @Override public void caretMoved(CaretEvent event) { // it's guaranteed that the document change event comes earlier than caret diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionManager.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionManager.java index bcb9fb50..82026941 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionManager.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionManager.java @@ -22,6 +22,8 @@ import com.microsoft.copilot.eclipse.core.completion.CompletionListener; import com.microsoft.copilot.eclipse.core.completion.CompletionProvider; import com.microsoft.copilot.eclipse.core.lsp.CopilotLanguageServerConnection; +import com.microsoft.copilot.eclipse.core.lsp.protocol.CompletionItem; +import com.microsoft.copilot.eclipse.core.lsp.protocol.NotifyShownParams; import com.microsoft.copilot.eclipse.ui.UiConstants; import com.microsoft.copilot.eclipse.ui.utils.SwtUtils; @@ -104,6 +106,7 @@ public void onCompletionResolved(CompletionCollection completions) { StyledText styledText = textViewer.getTextWidget(); if (styledText != null) { SwtUtils.invokeOnDisplayThread(styledText::redraw, styledText); + this.notifyShown(); } } @@ -185,6 +188,10 @@ public void acceptSuggestion() throws BadLocationException { this.document.replace(offset, 0, text); } + public CompletionCollection getCompletions() { + return completions; + } + /** * Dispose the resources used by the completion manager. */ @@ -197,4 +204,18 @@ public void dispose() { } } + private void notifyShown() { + if (this.completions == null || this.completions.getSize() == 0) { + return; + } + + CompletionItem item = this.completions.getCurrentItem(); + if (item == null) { + return; + } + + NotifyShownParams params = new NotifyShownParams(item.getUuid()); + this.lsConnection.notifyShown(params); + } + } diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/AcceptFullSuggestionHandler.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/AcceptFullSuggestionHandler.java index 52e31a46..ed356dd9 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/AcceptFullSuggestionHandler.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/AcceptFullSuggestionHandler.java @@ -3,6 +3,9 @@ import org.eclipse.core.commands.ExecutionEvent; import org.eclipse.core.commands.ExecutionException; +import com.microsoft.copilot.eclipse.core.completion.CompletionCollection; +import com.microsoft.copilot.eclipse.core.lsp.protocol.CompletionItem; +import com.microsoft.copilot.eclipse.core.lsp.protocol.NotifyAcceptedParams; import com.microsoft.copilot.eclipse.ui.completion.CompletionHandler; /** @@ -14,6 +17,7 @@ public class AcceptFullSuggestionHandler extends CopilotHandler { public Object execute(ExecutionEvent event) throws ExecutionException { CompletionHandler handler = getActiveCompletionHandler(); if (handler != null) { + notifyAccepted(handler.getCompletions()); handler.acceptFullSuggestion(); } return null; @@ -27,4 +31,15 @@ public boolean isEnabled() { } return false; } + + private void notifyAccepted(CompletionCollection completions) { + if (completions == null || completions.getSize() == 0) { + return; + } + + CompletionItem item = completions.getCurrentItem(); + String uuid = item.getUuid(); + NotifyAcceptedParams params = new NotifyAcceptedParams(uuid); + getLanguageServerConnection().notifyAccepted(params); + } } diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/CopilotHandler.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/CopilotHandler.java index 913fbbb8..61070cc2 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/CopilotHandler.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/CopilotHandler.java @@ -3,6 +3,8 @@ import org.eclipse.core.commands.AbstractHandler; import org.eclipse.jdt.annotation.Nullable; +import com.microsoft.copilot.eclipse.core.CopilotCore; +import com.microsoft.copilot.eclipse.core.lsp.CopilotLanguageServerConnection; import com.microsoft.copilot.eclipse.ui.CopilotUi; import com.microsoft.copilot.eclipse.ui.completion.CompletionHandler; import com.microsoft.copilot.eclipse.ui.completion.EditorsManager; @@ -26,4 +28,8 @@ public CompletionHandler getActiveCompletionHandler() { } return manager.getActiveCompletionHandler(); } + + public CopilotLanguageServerConnection getLanguageServerConnection() { + return CopilotCore.getPlugin().getCopilotLanguageServer(); + } } diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/DiscardSuggestionHandler.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/DiscardSuggestionHandler.java index aa46caf8..2eba32cf 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/DiscardSuggestionHandler.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/DiscardSuggestionHandler.java @@ -1,8 +1,12 @@ package com.microsoft.copilot.eclipse.ui.handlers; +import java.util.List; + import org.eclipse.core.commands.ExecutionEvent; import org.eclipse.core.commands.ExecutionException; +import com.microsoft.copilot.eclipse.core.completion.CompletionCollection; +import com.microsoft.copilot.eclipse.core.lsp.protocol.NotifyRejectedParams; import com.microsoft.copilot.eclipse.ui.completion.CompletionHandler; /** @@ -13,6 +17,7 @@ public class DiscardSuggestionHandler extends CopilotHandler { public Object execute(ExecutionEvent event) throws ExecutionException { CompletionHandler handler = getActiveCompletionHandler(); if (handler != null) { + notifyRejected(handler.getCompletions()); handler.clearCompletionRendering(); } return null; @@ -26,4 +31,17 @@ public boolean isEnabled() { } return false; } + + private void notifyRejected(CompletionCollection completions) { + if (completions == null) { + return; + } + List uuids = completions.getUuids(); + if (uuids == null || uuids.isEmpty()) { + return; + } + + NotifyRejectedParams params = new NotifyRejectedParams(uuids); + getLanguageServerConnection().notifyRejected(params); + } } From 2a3809c6dbf7bd782079d5c88a878e9e6d8b6944 Mon Sep 17 00:00:00 2001 From: Sheng Chen Date: Thu, 19 Dec 2024 16:23:48 +0800 Subject: [PATCH 024/690] fix - Index out of bound when drawing ghost text (#35) * fix - Index out of bound when drawing ghost text * The bug is because the offset in the model (IDocument) and in the widget (StyledText) can be different. Especially this will happen when some of the code are folded in the UI (like imports). So we need to convert the model offset to widget offset when we are drawing the ghost text. * Address comments --- .../eclipse/core/AuthStatusManagerTests.java | 5 ++-- .../ui/completion/CompletionHandler.java | 10 +++++-- .../ui/completion/CompletionManager.java | 21 ++++++++------ .../completion/EditorLifecycleListener.java | 29 +++++++++++-------- .../copilot/eclipse/ui/utils/SwtUtils.java | 4 +-- .../copilot/eclipse/ui/utils/UiUtils.java | 22 ++++++++------ target-platform.target | 28 +++++++----------- 7 files changed, 63 insertions(+), 56 deletions(-) diff --git a/com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/AuthStatusManagerTests.java b/com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/AuthStatusManagerTests.java index 416e1829..96d5f2d0 100644 --- a/com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/AuthStatusManagerTests.java +++ b/com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/AuthStatusManagerTests.java @@ -37,10 +37,10 @@ void testAuthStatusResultOnSuccess() { assertEquals(AuthStatusResult.OK, authStatusManager.getAuthStatusResult().getStatus()); } - + @Test void testCheckStatusLoadingWithDelay() throws InterruptedException { - String mockedUser = "mockedUser"; + String mockedUser = "mockedUser"; // Arrange AuthStatusResult expectedResult = new AuthStatusResult(); expectedResult.setStatus(AuthStatusResult.OK); @@ -55,7 +55,6 @@ void testCheckStatusLoadingWithDelay() throws InterruptedException { // Assert initial status is LOADING assertEquals(AuthStatusResult.LOADING, authStatusManager.getAuthStatusResult().getStatus()); - future.complete(expectedResult); // Assert final status is OK diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionHandler.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionHandler.java index 367dff8b..9eac539e 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionHandler.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionHandler.java @@ -137,6 +137,9 @@ public CompletionCollection getCompletions() { @Override public void caretMoved(CaretEvent event) { + int caretOffset = UiUtils.getCaretOffset(this.textViewer); + this.triggerPosition = new org.eclipse.jface.text.Position(caretOffset); + // it's guaranteed that the document change event comes earlier than caret // change event. See org.eclipse.swt.custom.StyledText#modifyContent() int currentVersion = this.lsConnection.getDocumentVersion(this.documentUri); @@ -152,8 +155,6 @@ public void caretMoved(CaretEvent event) { clearCompletionRendering(); } else { this.documentVersion = currentVersion; - int caretOffset = UiUtils.getCaretOffset(this.textViewer); - this.triggerPosition = new org.eclipse.jface.text.Position(caretOffset); triggerCompletion(); } @@ -178,7 +179,10 @@ private String getCategory() { * Disposes the resources of this completion handler. */ public void dispose() { - this.completionManager.dispose(); + if (this.completionManager != null) { + this.completionManager.dispose(); + this.completionManager = null; + } lsConnection.disconnectDocument(this.documentUri); try { this.document.removePositionCategory(this.getCategory()); diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionManager.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionManager.java index 82026941..e2e6d787 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionManager.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionManager.java @@ -24,8 +24,10 @@ import com.microsoft.copilot.eclipse.core.lsp.CopilotLanguageServerConnection; import com.microsoft.copilot.eclipse.core.lsp.protocol.CompletionItem; import com.microsoft.copilot.eclipse.core.lsp.protocol.NotifyShownParams; +import com.microsoft.copilot.eclipse.ui.CopilotUi; import com.microsoft.copilot.eclipse.ui.UiConstants; import com.microsoft.copilot.eclipse.ui.utils.SwtUtils; +import com.microsoft.copilot.eclipse.ui.utils.UiUtils; /** * A class to control completion rendering. @@ -75,6 +77,7 @@ public void triggerCompletion(Position position, int documentVersion) { this.provider.triggerCompletion(documentUri.toASCIIString(), LSPEclipseUtils.toPosition(position.getOffset(), this.document), documentVersion); } catch (BadLocationException e) { + CopilotUi.getPlugin().getLog().info("triggerCompletion BadLocationException e 77"); // TODO log & send telemetry } } @@ -118,20 +121,20 @@ public void paintControl(PaintEvent e) { } GC gc = e.gc; - setLineVerticalIndentation(styledText, gc); + int widgetOffset = UiUtils.modelOffset2WidgetOffset(textViewer, this.triggerPosition.getOffset()); + // will get index out of bounds if the cursor is at the end. + // Because there is no more text to get bounds at EOF. + widgetOffset = Math.max(Math.min(widgetOffset, styledText.getCharCount() - 1), 0); + setLineVerticalIndentation(styledText, gc, widgetOffset); if (this.completions == null) { return; } gc.setForeground(this.ghostTextColor); - // will get index out of bounds if the cursor is at the end. - // Because there is no more text to get bounds at EOF. - int caretOffset = Math.min(this.triggerPosition.getOffset(), styledText.getCharCount() - 1); - String firstLine = this.completions.getFirstLine(); if (StringUtils.isNotBlank(firstLine)) { - Rectangle bounds = styledText.getTextBounds(caretOffset, caretOffset); + Rectangle bounds = styledText.getTextBounds(widgetOffset, widgetOffset); int y = bounds.y; y += bounds.height - styledText.getLineHeight(); gc.drawString(firstLine, bounds.x + bounds.width, y, true); @@ -141,14 +144,14 @@ public void paintControl(PaintEvent e) { int lineHeight = styledText.getLineHeight(); int fontHeightt = gc.getFontMetrics().getHeight(); int x = styledText.getLeftMargin(); - Point offsetLocation = styledText.getLocationAtOffset(caretOffset); + Point offsetLocation = styledText.getLocationAtOffset(widgetOffset); int y = offsetLocation.y + lineHeight * 2 - fontHeightt; gc.drawText(remainingLines, x, y, true); } } - private void setLineVerticalIndentation(StyledText styledText, GC gc) { + private void setLineVerticalIndentation(StyledText styledText, GC gc, int widgetOffset) { int height = 0; if (this.completions != null) { // Change the height (line vertical indentation) to fit the line of @@ -158,7 +161,7 @@ private void setLineVerticalIndentation(StyledText styledText, GC gc) { height = ghostTextExtent.y - ghostTextExtent.y / numberOfLines; } - int lineIndex = styledText.getLineAtOffset(this.triggerPosition.getOffset()) + 1; + int lineIndex = styledText.getLineAtOffset(widgetOffset) + 1; lineIndex = Math.min(lineIndex, styledText.getLineCount() - 1); styledText.setLineVerticalIndent(lineIndex, height); } diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/EditorLifecycleListener.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/EditorLifecycleListener.java index 97857142..17f477e7 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/EditorLifecycleListener.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/EditorLifecycleListener.java @@ -1,5 +1,6 @@ package com.microsoft.copilot.eclipse.ui.completion; +import org.eclipse.ui.IEditorPart; import org.eclipse.ui.IPartListener; import org.eclipse.ui.IWorkbenchPart; import org.eclipse.ui.texteditor.ITextEditor; @@ -48,22 +49,26 @@ public void partOpened(IWorkbenchPart part) { * Creates the {@link CompletionHandler} for the ITextEditor of the IWorkbenchPart. */ public void createCompletionHandlerFor(IWorkbenchPart part) { - ITextEditor editor = part.getAdapter(ITextEditor.class); - if (editor == null) { - return; + IEditorPart editorPart = part.getAdapter(IEditorPart.class); + if (editorPart != null) { + ITextEditor editor = editorPart.getAdapter(ITextEditor.class); + if (editor != null) { + manager.getOrCreateCompletionHandlerFor(editor); + manager.setActiveEditor(editor); + } } - manager.getOrCreateCompletionHandlerFor(editor); - manager.setActiveEditor(editor); } void disposeCompletionHandlerFor(IWorkbenchPart part) { - ITextEditor editor = part.getAdapter(ITextEditor.class); - if (editor == null) { - return; - } - manager.disposeCompletionHandlerFor(editor); - if (editor.equals(manager.getActiveEditor())) { - manager.setActiveEditor(null); + IEditorPart editorPart = part.getAdapter(IEditorPart.class); + if (editorPart != null) { + ITextEditor editor = editorPart.getAdapter(ITextEditor.class); + if (editor != null) { + manager.disposeCompletionHandlerFor(editor); + if (editor.equals(manager.getActiveEditor())) { + manager.setActiveEditor(null); + } + } } } diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/utils/SwtUtils.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/utils/SwtUtils.java index 8bea0b49..6d8d7314 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/utils/SwtUtils.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/utils/SwtUtils.java @@ -72,12 +72,12 @@ public static IEditorPart getActiveEditorPart() { }); return ref.get(); } - + /** * This method retrieves the active workbench window from the event and then gets the shell associated with that * window. It is more specific to the Eclipse framework and is typically used in handlers for commands or actions * within the Eclipse environment. - + * * @throws ExecutionException if the active workbench window cannot be retrieved from the event. */ public static Shell getShellFromEvent(ExecutionEvent event) throws ExecutionException { diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/utils/UiUtils.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/utils/UiUtils.java index ad306c53..4beed572 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/utils/UiUtils.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/utils/UiUtils.java @@ -1,13 +1,12 @@ package com.microsoft.copilot.eclipse.ui.utils; import java.net.URI; -import java.util.concurrent.atomic.AtomicInteger; import org.eclipse.core.resources.IFile; import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jface.resource.ImageDescriptor; import org.eclipse.jface.text.ITextViewer; -import org.eclipse.swt.custom.StyledText; +import org.eclipse.jface.text.ITextViewerExtension5; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.graphics.ImageData; import org.eclipse.swt.graphics.ImageLoader; @@ -79,12 +78,17 @@ public static ImageDescriptor resizeIcon(String path, int width, int height) { * Gets the caret offset of the given text viewer. */ public static int getCaretOffset(ITextViewer textViewer) { - final AtomicInteger ref = new AtomicInteger(0); - StyledText styledText = textViewer.getTextWidget(); - SwtUtils.invokeOnDisplayThread(() -> { - int offset = styledText.getCaretOffset(); - ref.set(offset); - }, styledText); - return ref.get(); + if (textViewer == null) { + return 0; + } + return textViewer.getSelectedRange().x; + } + + /** + * Returns the widget offset that corresponds to the given offset in the viewer's input document or -1 if + * there is no such offset. + */ + public static int modelOffset2WidgetOffset(ITextViewer textViewer, int offset) { + return textViewer instanceof ITextViewerExtension5 extension ? extension.modelOffset2WidgetOffset(offset) : offset; } } diff --git a/target-platform.target b/target-platform.target index 220747c4..cf1e2950 100644 --- a/target-platform.target +++ b/target-platform.target @@ -2,8 +2,10 @@ - + + + + @@ -12,12 +14,8 @@ - - + org.apache.commons @@ -27,34 +25,28 @@ - + - - io.reactivex.rxjava3 - rxjava - 3.1.10 - org.mockito mockito-core 5.14.2 + jar org.mockito mockito-junit-jupiter 5.14.2 + jar - org.objenesis objenesis 3.4 + jar - + \ No newline at end of file From edb5d9d6cbc6863690ca9f0e900a75a26b4adbfa Mon Sep 17 00:00:00 2001 From: ethanyhou <149548697+ethanyhou@users.noreply.github.com> Date: Thu, 19 Dec 2024 16:57:10 +0800 Subject: [PATCH 025/690] ux - Updated icons to the plugin. (#34) --- .../build.properties | 3 ++- .../icons/copilot.png | Bin 707 -> 0 bytes .../icons/gitHub_copilot_error_blue.png | Bin 0 -> 744 bytes .../icons/gitHub_copilot_error_blue@2x.png | Bin 0 -> 1533 bytes .../icons/github_copilot_not_signed_in_blue.png | Bin 0 -> 616 bytes .../github_copilot_not_signed_in_blue@2x.png | Bin 0 -> 1002 bytes .../icons/github_copilot_signed_in_blue.png | Bin 0 -> 532 bytes .../icons/github_copilot_signed_in_blue@2x.png | Bin 0 -> 919 bytes .../icons/signin.png | Bin 0 -> 499 bytes .../icons/signin@2x.png | Bin 0 -> 1003 bytes .../icons/signout.png | Bin 0 -> 496 bytes .../icons/signout@2x.png | Bin 0 -> 1038 bytes com.microsoft.copilot.eclipse.ui/plugin.xml | 2 +- .../ui/handlers/ShowStatusBarMenuHandler.java | 9 +++++---- .../eclipse/ui/handlers/SignInHandler.java | 3 --- .../eclipse/ui/handlers/SignOutHandler.java | 4 +--- .../copilot/eclipse/ui/utils/UiUtils.java | 7 +++++++ 17 files changed, 16 insertions(+), 12 deletions(-) delete mode 100644 com.microsoft.copilot.eclipse.ui/icons/copilot.png create mode 100644 com.microsoft.copilot.eclipse.ui/icons/gitHub_copilot_error_blue.png create mode 100644 com.microsoft.copilot.eclipse.ui/icons/gitHub_copilot_error_blue@2x.png create mode 100644 com.microsoft.copilot.eclipse.ui/icons/github_copilot_not_signed_in_blue.png create mode 100644 com.microsoft.copilot.eclipse.ui/icons/github_copilot_not_signed_in_blue@2x.png create mode 100644 com.microsoft.copilot.eclipse.ui/icons/github_copilot_signed_in_blue.png create mode 100644 com.microsoft.copilot.eclipse.ui/icons/github_copilot_signed_in_blue@2x.png create mode 100644 com.microsoft.copilot.eclipse.ui/icons/signin.png create mode 100644 com.microsoft.copilot.eclipse.ui/icons/signin@2x.png create mode 100644 com.microsoft.copilot.eclipse.ui/icons/signout.png create mode 100644 com.microsoft.copilot.eclipse.ui/icons/signout@2x.png diff --git a/com.microsoft.copilot.eclipse.ui/build.properties b/com.microsoft.copilot.eclipse.ui/build.properties index 206f1966..7d0facb6 100644 --- a/com.microsoft.copilot.eclipse.ui/build.properties +++ b/com.microsoft.copilot.eclipse.ui/build.properties @@ -2,4 +2,5 @@ source.. = src output.. = target/classes bin.includes = META-INF/,\ .,\ - plugin.xml + plugin.xml,\ + icons/ diff --git a/com.microsoft.copilot.eclipse.ui/icons/copilot.png b/com.microsoft.copilot.eclipse.ui/icons/copilot.png deleted file mode 100644 index 31996ba7e667e0ea2d414b353a40a12ce7c41e53..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 707 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 literal 0 HcmV?d00001 diff --git a/com.microsoft.copilot.eclipse.ui/icons/github_copilot_not_signed_in_blue.png b/com.microsoft.copilot.eclipse.ui/icons/github_copilot_not_signed_in_blue.png new file mode 100644 index 0000000000000000000000000000000000000000..eb4f22c2224f7ef472a6b5224fd7d126a319a17a GIT binary patch literal 616 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 literal 0 HcmV?d00001 diff --git a/com.microsoft.copilot.eclipse.ui/icons/signin@2x.png b/com.microsoft.copilot.eclipse.ui/icons/signin@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..d72e0edaf9105f2ed1c2e701bdd576abd21a9021 GIT binary patch literal 1003 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 IM6N<$f)uaELjV8( literal 0 HcmV?d00001 diff --git a/com.microsoft.copilot.eclipse.ui/plugin.xml b/com.microsoft.copilot.eclipse.ui/plugin.xml index 50a70a5a..9ed13ce7 100644 --- a/com.microsoft.copilot.eclipse.ui/plugin.xml +++ b/com.microsoft.copilot.eclipse.ui/plugin.xml @@ -17,7 +17,7 @@ id="com.microsoft.copilot.eclipse.ui.statusBar"> diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/ShowStatusBarMenuHandler.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/ShowStatusBarMenuHandler.java index acf67332..72bfebfb 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/ShowStatusBarMenuHandler.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/ShowStatusBarMenuHandler.java @@ -25,14 +25,13 @@ public class ShowStatusBarMenuHandler extends AbstractHandler { private IHandlerService handlerService; - private ImageDescriptor icon; private AuthStatusManager authStatusManager; @Override public Object execute(ExecutionEvent event) throws ExecutionException { handlerService = HandlerUtil.getActiveWorkbenchWindow(event).getService(IHandlerService.class); authStatusManager = CopilotCore.getPlugin().getAuthStatusManager(); - icon = ImageDescriptor.createFromURL(UiUtils.class.getResource("/icons/copilot.png")); + MenuManager menuManager = new MenuManager(); addStatusAction(menuManager); @@ -75,10 +74,12 @@ private String getSignInStatusBasedOnAuthResult(AuthStatusResult authStatusResul private void addSignInOrSignOutAction(MenuManager menuManager) { if (authStatusManager.getAuthStatusResult().isSignedIn()) { - MenuActionFactory.createMenuAction(menuManager, Messages.menu_signOutFromGitHub, icon, handlerService, + ImageDescriptor signInIcon = UiUtils.buildImageDescriptorFromPngPath("/icons/signin.png"); + MenuActionFactory.createMenuAction(menuManager, Messages.menu_signOutFromGitHub, signInIcon, handlerService, "com.microsoft.copilot.eclipse.commands.signOut", true); } else { - MenuActionFactory.createMenuAction(menuManager, Messages.menu_signToGitHub, icon, handlerService, + ImageDescriptor signOutIcon = UiUtils.buildImageDescriptorFromPngPath("/icons/signout.png"); + MenuActionFactory.createMenuAction(menuManager, Messages.menu_signToGitHub, signOutIcon, handlerService, "com.microsoft.copilot.eclipse.commands.signIn", true); } } diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/SignInHandler.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/SignInHandler.java index 0c85a367..800a3334 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/SignInHandler.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/SignInHandler.java @@ -12,7 +12,6 @@ import com.microsoft.copilot.eclipse.core.AuthStatusManager; import com.microsoft.copilot.eclipse.core.CopilotCore; -import com.microsoft.copilot.eclipse.core.lsp.CopilotLanguageServerConnection; import com.microsoft.copilot.eclipse.core.lsp.protocol.SignInInitiateResult; import com.microsoft.copilot.eclipse.ui.dialogs.SignInConfirmDialog; import com.microsoft.copilot.eclipse.ui.dialogs.SignInDialog; @@ -27,14 +26,12 @@ public class SignInHandler extends AbstractHandler { private static final long SIGNIN_TIMEOUT_MILLIS = 180000L; - private CopilotLanguageServerConnection languageServer; private AuthStatusManager authStatusManager; /** * Initialize the Copilot Language Server for the SignInHandler. */ public SignInHandler() { - this.languageServer = CopilotCore.getPlugin().getCopilotLanguageServer(); this.authStatusManager = CopilotCore.getPlugin().getAuthStatusManager(); } diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/SignOutHandler.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/SignOutHandler.java index 0dd913d7..a0699ca7 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/SignOutHandler.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/SignOutHandler.java @@ -19,14 +19,12 @@ */ public class SignOutHandler extends AbstractHandler { - private CopilotLanguageServerConnection languageServer; private AuthStatusManager authStatusManager; /** * Initialize the Copilot Language Server and Auth Status Manager for the SignOutHandler. */ public SignOutHandler() { - this.languageServer = CopilotCore.getPlugin().getCopilotLanguageServer(); this.authStatusManager = CopilotCore.getPlugin().getAuthStatusManager(); } @@ -34,7 +32,7 @@ public SignOutHandler() { public Object execute(ExecutionEvent event) throws ExecutionException { Shell shell = SwtUtils.getShellFromEvent(event); try { - AuthStatusResult result = this.languageServer.signOut().get(); + AuthStatusResult result = authStatusManager.signOut(); if (!result.isSignedIn()) { showSignOutMessage(shell); authStatusManager.checkStatus(); diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/utils/UiUtils.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/utils/UiUtils.java index 4beed572..4df81a79 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/utils/UiUtils.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/utils/UiUtils.java @@ -91,4 +91,11 @@ public static int getCaretOffset(ITextViewer textViewer) { public static int modelOffset2WidgetOffset(ITextViewer textViewer, int offset) { return textViewer instanceof ITextViewerExtension5 extension ? extension.modelOffset2WidgetOffset(offset) : offset; } + + /** + * Builds an image descriptor from a PNG file at the given path. + */ + public static final ImageDescriptor buildImageDescriptorFromPngPath(String path) { + return ImageDescriptor.createFromURL(UiUtils.class.getResource(path)); + } } From d31a5a7f97a65c2f9e3e5931c8bee663c3d3bb15 Mon Sep 17 00:00:00 2001 From: Sheng Chen Date: Thu, 19 Dec 2024 18:49:43 +0800 Subject: [PATCH 026/690] build - Add feature and repository module to build installation bundle (#36) --- .azure-pipelines/nightly.yml | 84 +++++++++++++++++++ .../build.properties | 1 + .../feature.xml | 19 +++++ com.microsoft.copilot.eclipse.feature/pom.xml | 13 +++ .../category.xml | 8 ++ .../pom.xml | 37 ++++++++ .../build.properties | 3 +- pom.xml | 4 + 8 files changed, 168 insertions(+), 1 deletion(-) create mode 100644 .azure-pipelines/nightly.yml create mode 100644 com.microsoft.copilot.eclipse.feature/build.properties create mode 100644 com.microsoft.copilot.eclipse.feature/feature.xml create mode 100644 com.microsoft.copilot.eclipse.feature/pom.xml create mode 100644 com.microsoft.copilot.eclipse.repository/category.xml create mode 100644 com.microsoft.copilot.eclipse.repository/pom.xml diff --git a/.azure-pipelines/nightly.yml b/.azure-pipelines/nightly.yml new file mode 100644 index 00000000..10e2f133 --- /dev/null +++ b/.azure-pipelines/nightly.yml @@ -0,0 +1,84 @@ +name: $(Date:yyyyMMdd).$(Rev:r) +variables: + - name: Codeql.Enabled + value: true +resources: + repositories: + - repository: self + type: git + ref: refs/heads/main + - repository: MicroBuildTemplate + type: git + name: 1ESPipelineTemplates/MicroBuildTemplate +trigger: none +extends: + template: azure-pipelines/MicroBuild.1ES.Official.yml@MicroBuildTemplate + parameters: + pool: + os: linux + name: 1ES_JavaTooling_Pool + image: 1ES_JavaTooling_Ubuntu-2004 + sdl: + sourceAnalysisPool: + name: 1ES_JavaTooling_Pool + image: 1ES_JavaTooling_Windows_2022 + os: windows + stages: + - stage: Build + jobs: + - job: Build + displayName: GitHub-Copilot-Eclipse-Nightly + templateContext: + outputs: + - output: pipelineArtifact + artifactName: plugin + targetPath: '$(Build.ArtifactStagingDirectory)/plugin' + displayName: "Publish Artifact: plugin" + steps: + - checkout: self + fetchTags: false + + - task: JavaToolInstaller@0 + displayName: Use Java 17 + inputs: + versionSpec: "17" + jdkArchitectureOption: x64 + jdkSourceOption: PreInstalled + + - task: UseNode@1 + displayName: Use Node 20.x + inputs: + version: '20.x' + + - bash: npm i + workingDirectory: com.microsoft.copilot.eclipse.core/copilot-agent + displayName: Install Copilot LS + + - bash: ./mvnw clean package + displayName: 'Run Maven Clean and Package' + + # TODO: support code sign + - bash: | + mkdir -p ./artifacts/eclipse/ + cp ./com.microsoft.copilot.eclipse.repository/target/com.microsoft.copilot.eclipse.repository*.zip ./artifacts/eclipse/GithubCopilotForEclipse.zip + + # unzip ./artifacts/eclipse/GithubCopilotForEclipse.zip "**/*.jar" "*.jar" -d ./artifacts/eclipse/folder + # rm ./artifacts/eclipse/GithubCopilotForEclipse.zip + + # ## Workaround: Remove MD5/SHA256 Validation in artifacts.xml + # cd ./artifacts/eclipse/folder + # unzip artifacts.jar -d ./artifacts + # rm artifacts.jar + # cd artifacts + # sed -i -E '/download\.md5|checksum/d' ./artifacts.xml + # zip -R ./artifacts.jar * **/* + # mv ./artifacts.jar ../artifacts.jar + # cd .. + # rm -rf ./artifacts + + - task: CopyFiles@2 + displayName: Copy plugin zip + inputs: + Contents: | + artifacts/eclipse/GithubCopilotForEclipse.zip + TargetFolder: '$(Build.ArtifactStagingDirectory)/plugin' \ No newline at end of file diff --git a/com.microsoft.copilot.eclipse.feature/build.properties b/com.microsoft.copilot.eclipse.feature/build.properties new file mode 100644 index 00000000..64f93a9f --- /dev/null +++ b/com.microsoft.copilot.eclipse.feature/build.properties @@ -0,0 +1 @@ +bin.includes = feature.xml diff --git a/com.microsoft.copilot.eclipse.feature/feature.xml b/com.microsoft.copilot.eclipse.feature/feature.xml new file mode 100644 index 00000000..589fcb30 --- /dev/null +++ b/com.microsoft.copilot.eclipse.feature/feature.xml @@ -0,0 +1,19 @@ + + + + + + \ No newline at end of file diff --git a/com.microsoft.copilot.eclipse.feature/pom.xml b/com.microsoft.copilot.eclipse.feature/pom.xml new file mode 100644 index 00000000..4ef0a422 --- /dev/null +++ b/com.microsoft.copilot.eclipse.feature/pom.xml @@ -0,0 +1,13 @@ + + 4.0.0 + + com.microsoft.copilot.eclipse + github-copilot-for-eclipse + 0.1.0-SNAPSHOT + + com.microsoft.copilot.eclipse.feature + eclipse-feature + ${base.name} :: Feature + \ No newline at end of file diff --git a/com.microsoft.copilot.eclipse.repository/category.xml b/com.microsoft.copilot.eclipse.repository/category.xml new file mode 100644 index 00000000..1a640463 --- /dev/null +++ b/com.microsoft.copilot.eclipse.repository/category.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/com.microsoft.copilot.eclipse.repository/pom.xml b/com.microsoft.copilot.eclipse.repository/pom.xml new file mode 100644 index 00000000..477bf734 --- /dev/null +++ b/com.microsoft.copilot.eclipse.repository/pom.xml @@ -0,0 +1,37 @@ + + 4.0.0 + + com.microsoft.copilot.eclipse + github-copilot-for-eclipse + 0.1.0-SNAPSHOT + + com.microsoft.copilot.eclipse.repository + eclipse-repository + ${base.name} :: Repository + + + + + org.eclipse.tycho + tycho-p2-director-plugin + ${tycho-version} + + + materialize-products + + materialize-products + + + + archive-products + + archive-products + + + + + + + \ No newline at end of file diff --git a/com.microsoft.copilot.eclipse.ui/build.properties b/com.microsoft.copilot.eclipse.ui/build.properties index 7d0facb6..0086cac1 100644 --- a/com.microsoft.copilot.eclipse.ui/build.properties +++ b/com.microsoft.copilot.eclipse.ui/build.properties @@ -3,4 +3,5 @@ output.. = target/classes bin.includes = META-INF/,\ .,\ plugin.xml,\ - icons/ + icons/,\ + plugin.properties diff --git a/pom.xml b/pom.xml index 1ff5f28c..aa586a87 100644 --- a/pom.xml +++ b/pom.xml @@ -20,6 +20,10 @@ com.microsoft.copilot.eclipse.core com.microsoft.copilot.eclipse.ui + + com.microsoft.copilot.eclipse.feature + com.microsoft.copilot.eclipse.repository + com.microsoft.copilot.eclipse.core.test com.microsoft.copilot.eclipse.ui.test From 4d7c1f0565853a58c460cff917b8cdeb1992e0e2 Mon Sep 17 00:00:00 2001 From: ethanyhou <149548697+ethanyhou@users.noreply.github.com> Date: Thu, 19 Dec 2024 19:06:01 +0800 Subject: [PATCH 027/690] refactor - Renamed authStatusManager to copilotStatusManager for broader usage. (#37) --- ...ts.java => CopilotStatusManagerTests.java} | 38 +++++++++--------- .../copilot/eclipse/core/CopilotCore.java | 10 ++--- ...Manager.java => CopilotStatusManager.java} | 40 +++++++++---------- .../core/lsp/CopilotLanguageServer.java | 8 ++-- .../lsp/CopilotLanguageServerConnection.java | 22 +++++----- ...usResult.java => CopilotStatusResult.java} | 4 +- .../copilot/eclipse/ui/CopilotUi.java | 14 +++++++ .../completion/CompletionStatusListener.java | 16 ++++++++ .../ui/dialogs/SignInConfirmDialog.java | 8 ++-- .../ui/handlers/ShowStatusBarMenuHandler.java | 30 +++++++------- .../eclipse/ui/handlers/SignInHandler.java | 8 ++-- .../eclipse/ui/handlers/SignOutHandler.java | 13 +++--- 12 files changed, 120 insertions(+), 91 deletions(-) rename com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/{AuthStatusManagerTests.java => CopilotStatusManagerTests.java} (51%) rename com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/{AuthStatusManager.java => CopilotStatusManager.java} (59%) rename com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/{AuthStatusResult.java => CopilotStatusResult.java} (95%) create mode 100644 com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionStatusListener.java diff --git a/com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/AuthStatusManagerTests.java b/com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/CopilotStatusManagerTests.java similarity index 51% rename from com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/AuthStatusManagerTests.java rename to com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/CopilotStatusManagerTests.java index 96d5f2d0..e5c33e01 100644 --- a/com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/AuthStatusManagerTests.java +++ b/com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/CopilotStatusManagerTests.java @@ -13,65 +13,65 @@ import org.mockito.junit.jupiter.MockitoExtension; import com.microsoft.copilot.eclipse.core.lsp.CopilotLanguageServerConnection; -import com.microsoft.copilot.eclipse.core.lsp.protocol.AuthStatusResult; +import com.microsoft.copilot.eclipse.core.lsp.protocol.CopilotStatusResult; @ExtendWith(MockitoExtension.class) -class AuthStatusManagerTests { +class CopilotStatusManagerTests { @Mock CopilotLanguageServerConnection mockConnection; - AuthStatusManager authStatusManager; + CopilotStatusManager copilotStatusManager; @BeforeEach public void setUp() { - authStatusManager = new AuthStatusManager(mockConnection); + copilotStatusManager = new CopilotStatusManager(mockConnection); } @Test - void testAuthStatusResultOnSuccess() { - AuthStatusResult expectedResult = new AuthStatusResult(); - expectedResult.setStatus(AuthStatusResult.OK); + void testCopilotStatusResultOnSuccess() { + CopilotStatusResult expectedResult = new CopilotStatusResult(); + expectedResult.setStatus(CopilotStatusResult.OK); when(mockConnection.checkStatus(false)).thenReturn(CompletableFuture.completedFuture(expectedResult)); - authStatusManager.checkStatus(); + copilotStatusManager.checkStatus(); - assertEquals(AuthStatusResult.OK, authStatusManager.getAuthStatusResult().getStatus()); + assertEquals(CopilotStatusResult.OK, copilotStatusManager.getCopilotStatusResult().getStatus()); } @Test void testCheckStatusLoadingWithDelay() throws InterruptedException { String mockedUser = "mockedUser"; // Arrange - AuthStatusResult expectedResult = new AuthStatusResult(); - expectedResult.setStatus(AuthStatusResult.OK); + CopilotStatusResult expectedResult = new CopilotStatusResult(); + expectedResult.setStatus(CopilotStatusResult.OK); expectedResult.setUser(mockedUser); - CompletableFuture future = new CompletableFuture<>(); + CompletableFuture future = new CompletableFuture<>(); when(mockConnection.checkStatus(false)).thenReturn(future); // Act - authStatusManager.checkStatus(); + copilotStatusManager.checkStatus(); // Assert initial status is LOADING - assertEquals(AuthStatusResult.LOADING, authStatusManager.getAuthStatusResult().getStatus()); + assertEquals(CopilotStatusResult.LOADING, copilotStatusManager.getCopilotStatusResult().getStatus()); future.complete(expectedResult); // Assert final status is OK - assertEquals(AuthStatusResult.OK, authStatusManager.getAuthStatusResult().getStatus()); - assertEquals(mockedUser, authStatusManager.getAuthStatusResult().getUser()); + assertEquals(CopilotStatusResult.OK, copilotStatusManager.getCopilotStatusResult().getStatus()); + assertEquals(mockedUser, copilotStatusManager.getCopilotStatusResult().getUser()); } @Test void testCheckStatusError() { - CompletableFuture future = new CompletableFuture<>(); + CompletableFuture future = new CompletableFuture<>(); future.completeExceptionally(new CompletionException(new Exception("Some other error"))); when(mockConnection.checkStatus(false)).thenReturn(future); - authStatusManager.checkStatus(); + copilotStatusManager.checkStatus(); - assertEquals(AuthStatusResult.ERROR, authStatusManager.getAuthStatusResult().getStatus()); + assertEquals(CopilotStatusResult.ERROR, copilotStatusManager.getCopilotStatusResult().getStatus()); } } diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/CopilotCore.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/CopilotCore.java index 6981e2bb..0b3501eb 100644 --- a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/CopilotCore.java +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/CopilotCore.java @@ -20,7 +20,7 @@ public class CopilotCore extends Plugin { private CopilotLanguageServerConnection copilotLanguageServer; - private AuthStatusManager authStatusManager; + private CopilotStatusManager copilotStatusManager; private CompletionProvider completionProvider; private static CopilotCore COPILOT_CORE_PLUGIN = null; @@ -64,9 +64,9 @@ void init() { LanguageServerWrapper wrapper = LanguageServiceAccessor.startLanguageServer(serverDef); this.copilotLanguageServer = new CopilotLanguageServerConnection(wrapper); this.completionProvider = new CompletionProvider(this.copilotLanguageServer); - this.authStatusManager = new AuthStatusManager(this.copilotLanguageServer); + this.copilotStatusManager = new CopilotStatusManager(this.copilotLanguageServer); - this.authStatusManager.checkStatus(); + this.copilotStatusManager.checkStatus(); }; Job initJob = new Job("GitHub Copilot Initialization...") { @@ -83,8 +83,8 @@ public CopilotLanguageServerConnection getCopilotLanguageServer() { return copilotLanguageServer; } - public AuthStatusManager getAuthStatusManager() { - return authStatusManager; + public CopilotStatusManager getCopilotStatusManager() { + return copilotStatusManager; } public CompletionProvider getCompletionProvider() { diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/AuthStatusManager.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/CopilotStatusManager.java similarity index 59% rename from com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/AuthStatusManager.java rename to com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/CopilotStatusManager.java index 2a0cc3ff..6693a750 100644 --- a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/AuthStatusManager.java +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/CopilotStatusManager.java @@ -5,29 +5,29 @@ import java.util.concurrent.TimeUnit; import com.microsoft.copilot.eclipse.core.lsp.CopilotLanguageServerConnection; -import com.microsoft.copilot.eclipse.core.lsp.protocol.AuthStatusResult; +import com.microsoft.copilot.eclipse.core.lsp.protocol.CopilotStatusResult; import com.microsoft.copilot.eclipse.core.lsp.protocol.SignInInitiateResult; /** * Manager for the authentication status. */ -public class AuthStatusManager { +public class CopilotStatusManager { private CopilotLanguageServerConnection connection; - private AuthStatusResult authStatusResult; + private CopilotStatusResult copilotStatusResult; private static final int CHECK_STATUS_TIMEOUT_MILLIS = 3000; /** - * Constructor for the AuthStatusManager. + * Constructor for the CopilotStatusManager. * * @param connection the connection to the language server. */ - public AuthStatusManager(CopilotLanguageServerConnection connection) { + public CopilotStatusManager(CopilotLanguageServerConnection connection) { this.connection = connection; - this.authStatusResult = new AuthStatusResult(); - this.authStatusResult.setStatus(AuthStatusResult.LOADING); + this.copilotStatusResult = new CopilotStatusResult(); + this.copilotStatusResult.setStatus(CopilotStatusResult.LOADING); } /** @@ -39,7 +39,7 @@ public AuthStatusManager(CopilotLanguageServerConnection connection) { public SignInInitiateResult signInInitiate() throws InterruptedException, ExecutionException { SignInInitiateResult result = connection.signInInitiate().get(); if (result.isAlreadySignedIn()) { - this.authStatusResult.setStatus(AuthStatusResult.OK); + this.copilotStatusResult.setStatus(CopilotStatusResult.OK); } return result; } @@ -50,11 +50,11 @@ public SignInInitiateResult signInInitiate() throws InterruptedException, Execut * @throws ExecutionException if the sign in process fails due to an execution error * @throws InterruptedException if the sign in process is interrupted */ - public AuthStatusResult signInConfirm(String userCode) throws InterruptedException, ExecutionException { - AuthStatusResult result = connection.signInConfirm(userCode).get(); + public CopilotStatusResult signInConfirm(String userCode) throws InterruptedException, ExecutionException { + CopilotStatusResult result = connection.signInConfirm(userCode).get(); if (result.isSignedIn()) { - this.authStatusResult.setStatus(AuthStatusResult.OK); - this.authStatusResult.setUser(result.getUser()); + this.copilotStatusResult.setStatus(CopilotStatusResult.OK); + this.copilotStatusResult.setUser(result.getUser()); } return result; } @@ -65,10 +65,10 @@ public AuthStatusResult signInConfirm(String userCode) throws InterruptedExcepti * @throws ExecutionException if the sign out process fails due to an execution error * @throws InterruptedException if the sign out process is interrupted */ - public AuthStatusResult signOut() throws InterruptedException, ExecutionException { - AuthStatusResult result = connection.signOut().get(); + public CopilotStatusResult signOut() throws InterruptedException, ExecutionException { + CopilotStatusResult result = connection.signOut().get(); if (!result.isSignedIn()) { - this.authStatusResult.setStatus(AuthStatusResult.NOT_SIGNED_IN); + this.copilotStatusResult.setStatus(CopilotStatusResult.NOT_SIGNED_IN); } return result; } @@ -77,19 +77,19 @@ public AuthStatusResult signOut() throws InterruptedException, ExecutionExceptio * Check the login status for current machine. */ public void checkStatus() { - CompletableFuture statusFuture = this.connection.checkStatus(false); + CompletableFuture statusFuture = this.connection.checkStatus(false); statusFuture.orTimeout(CHECK_STATUS_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS).thenAccept(result -> { - this.authStatusResult = result; + this.copilotStatusResult = result; }).exceptionally(ex -> { // TODO: log & send telemetry - this.authStatusResult.setStatus(AuthStatusResult.ERROR); + this.copilotStatusResult.setStatus(CopilotStatusResult.ERROR); return null; }); } - public AuthStatusResult getAuthStatusResult() { - return this.authStatusResult; + public CopilotStatusResult getCopilotStatusResult() { + return this.copilotStatusResult; } } diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/CopilotLanguageServer.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/CopilotLanguageServer.java index 5368266c..1818cbd5 100644 --- a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/CopilotLanguageServer.java +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/CopilotLanguageServer.java @@ -5,10 +5,10 @@ import org.eclipse.lsp4j.jsonrpc.services.JsonRequest; import org.eclipse.lsp4j.services.LanguageServer; -import com.microsoft.copilot.eclipse.core.lsp.protocol.AuthStatusResult; import com.microsoft.copilot.eclipse.core.lsp.protocol.CheckStatusParams; import com.microsoft.copilot.eclipse.core.lsp.protocol.CompletionParams; import com.microsoft.copilot.eclipse.core.lsp.protocol.CompletionResult; +import com.microsoft.copilot.eclipse.core.lsp.protocol.CopilotStatusResult; import com.microsoft.copilot.eclipse.core.lsp.protocol.NotifyAcceptedParams; import com.microsoft.copilot.eclipse.core.lsp.protocol.NotifyRejectedParams; import com.microsoft.copilot.eclipse.core.lsp.protocol.NotifyShownParams; @@ -25,7 +25,7 @@ public interface CopilotLanguageServer extends LanguageServer { * Check the login status for current machine. */ @JsonRequest - CompletableFuture checkStatus(CheckStatusParams param); + CompletableFuture checkStatus(CheckStatusParams param); /** * Get single completion for the given parameters. @@ -43,13 +43,13 @@ public interface CopilotLanguageServer extends LanguageServer { * Confirm the sign in process. */ @JsonRequest - CompletableFuture signInConfirm(SignInConfirmParams param); + CompletableFuture signInConfirm(SignInConfirmParams param); /** * Sign out the current user. */ @JsonRequest - CompletableFuture signOut(NullParams params); + CompletableFuture signOut(NullParams params); /** * Notify the language server that the completion was shown. diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/CopilotLanguageServerConnection.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/CopilotLanguageServerConnection.java index 0a1e6f02..c565a8bf 100644 --- a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/CopilotLanguageServerConnection.java +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/CopilotLanguageServerConnection.java @@ -9,11 +9,11 @@ import org.eclipse.lsp4e.LanguageServerWrapper; import org.eclipse.lsp4j.services.LanguageServer; -import com.microsoft.copilot.eclipse.core.AuthStatusManager; -import com.microsoft.copilot.eclipse.core.lsp.protocol.AuthStatusResult; +import com.microsoft.copilot.eclipse.core.CopilotStatusManager; import com.microsoft.copilot.eclipse.core.lsp.protocol.CheckStatusParams; import com.microsoft.copilot.eclipse.core.lsp.protocol.CompletionParams; import com.microsoft.copilot.eclipse.core.lsp.protocol.CompletionResult; +import com.microsoft.copilot.eclipse.core.lsp.protocol.CopilotStatusResult; import com.microsoft.copilot.eclipse.core.lsp.protocol.NotifyAcceptedParams; import com.microsoft.copilot.eclipse.core.lsp.protocol.NotifyRejectedParams; import com.microsoft.copilot.eclipse.core.lsp.protocol.NotifyShownParams; @@ -65,8 +65,8 @@ public int getDocumentVersion(URI uri) { /** * Check the login status for current machine. */ - public CompletableFuture checkStatus(Boolean localCheckOnly) { - Function> fn = server -> { + public CompletableFuture checkStatus(Boolean localCheckOnly) { + Function> fn = server -> { CheckStatusParams param = new CheckStatusParams(); param.setLocalChecksOnly(localCheckOnly); return ((CopilotLanguageServer) server).checkStatus(param); @@ -84,7 +84,7 @@ public CompletableFuture getCompletions(CompletionParams param } /** - * Please use the {@link AuthStatusManager#signInInitiate()} method instead. + * Please use the {@link CopilotStatusManager#signInInitiate()} method instead. *

* Initiate the sign in process. */ @@ -95,12 +95,12 @@ public CompletableFuture signInInitiate() { } /** - * Please use the {@link AuthStatusManager#signInConfirm()} method instead. + * Please use the {@link CopilotStatusManager#signInConfirm()} method instead. *

* Confirm the sign in process. */ - public CompletableFuture signInConfirm(String userCode) { - Function> fn = (server) -> { + public CompletableFuture signInConfirm(String userCode) { + Function> fn = (server) -> { SignInConfirmParams param = new SignInConfirmParams(userCode); return ((CopilotLanguageServer) server).signInConfirm(param); }; @@ -108,12 +108,12 @@ public CompletableFuture signInConfirm(String userCode) { } /** - * Please use the {@link AuthStatusManager#signOut()} method instead. + * Please use the {@link CopilotStatusManager#signOut()} method instead. *

* Sign out from the GitHub Copilot. */ - public CompletableFuture signOut() { - Function> fn = (server) -> ((CopilotLanguageServer) server) + public CompletableFuture signOut() { + Function> fn = (server) -> ((CopilotLanguageServer) server) .signOut(new NullParams()); return this.languageServerWrapper.execute(fn); } diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/AuthStatusResult.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/CopilotStatusResult.java similarity index 95% rename from com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/AuthStatusResult.java rename to com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/CopilotStatusResult.java index 83a637e2..bc201a80 100644 --- a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/AuthStatusResult.java +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/CopilotStatusResult.java @@ -9,7 +9,7 @@ /** * Result for the Authentication status. */ -public class AuthStatusResult { +public class CopilotStatusResult { public static final String OK = "OK"; public static final String ERROR = "Error"; @@ -79,7 +79,7 @@ public boolean equals(Object obj) { if (getClass() != obj.getClass()) { return false; } - AuthStatusResult other = (AuthStatusResult) obj; + CopilotStatusResult other = (CopilotStatusResult) obj; return Objects.equals(status, other.status) && Objects.equals(user, other.user); } diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/CopilotUi.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/CopilotUi.java index 9a36a17b..cda7ecd1 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/CopilotUi.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/CopilotUi.java @@ -7,7 +7,10 @@ import org.osgi.framework.BundleContext; import com.microsoft.copilot.eclipse.core.CopilotCore; +import com.microsoft.copilot.eclipse.core.completion.CompletionListener; +import com.microsoft.copilot.eclipse.core.completion.CompletionProvider; import com.microsoft.copilot.eclipse.core.lsp.CopilotLanguageServerConnection; +import com.microsoft.copilot.eclipse.ui.completion.CompletionStatusListener; import com.microsoft.copilot.eclipse.ui.completion.EditorLifecycleListener; import com.microsoft.copilot.eclipse.ui.completion.EditorsManager; import com.microsoft.copilot.eclipse.ui.utils.SwtUtils; @@ -20,6 +23,7 @@ public class CopilotUi extends Plugin { private static final int RETRY_COUNT = 30; private static CopilotUi COPILOT_UI_PLUGIN = null; + private CompletionStatusListener completionStatusListener; private EditorLifecycleListener editorLifecycleListener; private EditorsManager editorsManager; @@ -55,8 +59,10 @@ public void start(BundleContext context) throws Exception { this.editorsManager = new EditorsManager(connection, CopilotCore.getPlugin().getCompletionProvider()); this.editorLifecycleListener = new EditorLifecycleListener(editorsManager); + this.completionStatusListener = new CompletionStatusListener(); registerPartListener(); + registerCompletionListener(); // Initialize the completion handler for the active editor in case we miss the event // to initialize it. @@ -82,6 +88,10 @@ private void registerPartListener() { } } + private void registerCompletionListener() { + CopilotCore.getPlugin().getCompletionProvider().addCompletionListener(this.completionStatusListener); + } + private void initCompletionHandlerForActiveEditor() { IEditorPart editorPart = SwtUtils.getActiveEditorPart(); if (editorPart != null) { @@ -95,5 +105,9 @@ private void unregisterPartListener() { window.getPartService().removePartListener(this.editorLifecycleListener); } } + + private void unregisterCompletionListener() { + CopilotCore.getPlugin().getCompletionProvider().removeCompletionListener(this.completionStatusListener); + } } diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionStatusListener.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionStatusListener.java new file mode 100644 index 00000000..01a6e637 --- /dev/null +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionStatusListener.java @@ -0,0 +1,16 @@ +package com.microsoft.copilot.eclipse.ui.completion; + +import com.microsoft.copilot.eclipse.core.completion.CompletionCollection; +import com.microsoft.copilot.eclipse.core.completion.CompletionListener; +import com.microsoft.copilot.eclipse.ui.utils.UiUtils; + +/** + * Listener for tracking copilot completion status. + */ +public class CompletionStatusListener implements CompletionListener { + + @Override + public void onCompletionResolved(CompletionCollection completions) { + // do nothing + } +} \ No newline at end of file diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/dialogs/SignInConfirmDialog.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/dialogs/SignInConfirmDialog.java index 998ca5f5..417ef6d0 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/dialogs/SignInConfirmDialog.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/dialogs/SignInConfirmDialog.java @@ -12,7 +12,7 @@ import org.eclipse.swt.widgets.Shell; import com.microsoft.copilot.eclipse.core.CopilotCore; -import com.microsoft.copilot.eclipse.core.lsp.protocol.AuthStatusResult; +import com.microsoft.copilot.eclipse.core.lsp.protocol.CopilotStatusResult; import com.microsoft.copilot.eclipse.ui.i18n.Messages; /** @@ -22,7 +22,7 @@ public class SignInConfirmDialog extends ProgressMonitorDialog { private final String userCode; private final long timeout; - private CompletableFuture future; + private CompletableFuture future; private IStatus status; /** @@ -73,7 +73,7 @@ public void run(IProgressMonitor monitor) throws InvocationTargetException, Inte try { future = CompletableFuture.supplyAsync(() -> { try { - return CopilotCore.getPlugin().getAuthStatusManager().signInConfirm(userCode); + return CopilotCore.getPlugin().getCopilotStatusManager().signInConfirm(userCode); } catch (Exception e) { // TODO: log & send telemetry return null; @@ -118,7 +118,7 @@ private void waitForAuthorization(IProgressMonitor monitor) throws InterruptedEx } private void handleAuthorizationResult() throws ExecutionException, InterruptedException { - AuthStatusResult result = future.get(); + CopilotStatusResult result = future.get(); String errorMsg = null; if (result == null || !result.isSignedIn()) { diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/ShowStatusBarMenuHandler.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/ShowStatusBarMenuHandler.java index 72bfebfb..23461903 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/ShowStatusBarMenuHandler.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/ShowStatusBarMenuHandler.java @@ -13,9 +13,9 @@ import org.eclipse.ui.handlers.HandlerUtil; import org.eclipse.ui.handlers.IHandlerService; -import com.microsoft.copilot.eclipse.core.AuthStatusManager; import com.microsoft.copilot.eclipse.core.CopilotCore; -import com.microsoft.copilot.eclipse.core.lsp.protocol.AuthStatusResult; +import com.microsoft.copilot.eclipse.core.CopilotStatusManager; +import com.microsoft.copilot.eclipse.core.lsp.protocol.CopilotStatusResult; import com.microsoft.copilot.eclipse.ui.i18n.Messages; import com.microsoft.copilot.eclipse.ui.utils.UiUtils; @@ -25,17 +25,17 @@ public class ShowStatusBarMenuHandler extends AbstractHandler { private IHandlerService handlerService; - private AuthStatusManager authStatusManager; + private CopilotStatusManager copilotStatusManager; @Override public Object execute(ExecutionEvent event) throws ExecutionException { handlerService = HandlerUtil.getActiveWorkbenchWindow(event).getService(IHandlerService.class); - authStatusManager = CopilotCore.getPlugin().getAuthStatusManager(); + copilotStatusManager = CopilotCore.getPlugin().getCopilotStatusManager(); MenuManager menuManager = new MenuManager(); addStatusAction(menuManager); - if (!authStatusManager.getAuthStatusResult().isLoading()) { + if (!copilotStatusManager.getCopilotStatusResult().isLoading()) { menuManager.add(new Separator()); addSignInOrSignOutAction(menuManager); } @@ -47,25 +47,25 @@ public Object execute(ExecutionEvent event) throws ExecutionException { } private void addStatusAction(MenuManager menuManager) { - String signInStatus = getSignInStatusBasedOnAuthResult(authStatusManager.getAuthStatusResult()); + String signInStatus = getSignInStatusBasedOnAuthResult(copilotStatusManager.getCopilotStatusResult()); String signInStatusTitle = Messages.menu_signInStatus + ": " + signInStatus; MenuActionFactory.createMenuAction(menuManager, signInStatusTitle, handlerService, signInStatus, false); } - private String getSignInStatusBasedOnAuthResult(AuthStatusResult authStatusResult) { - switch (authStatusResult.getStatus()) { - case AuthStatusResult.OK: + private String getSignInStatusBasedOnAuthResult(CopilotStatusResult copilotStatusResult) { + switch (copilotStatusResult.getStatus()) { + case CopilotStatusResult.OK: return Messages.menu_signInStatus_ready; - case AuthStatusResult.ERROR: + case CopilotStatusResult.ERROR: return Messages.menu_signInStatus_unknownError; - case AuthStatusResult.LOADING: + case CopilotStatusResult.LOADING: return Messages.menu_signInStatus_loading; - case AuthStatusResult.NOT_SIGNED_IN: + case CopilotStatusResult.NOT_SIGNED_IN: return Messages.menu_signInStatus_notSignedInToGitHub; - case AuthStatusResult.WARNING: + case CopilotStatusResult.WARNING: return Messages.menu_signInStatus_agentWarning; - case AuthStatusResult.NOT_AUTHORIZED: + case CopilotStatusResult.NOT_AUTHORIZED: return Messages.menu_signInStatus_notAuthorized; default: return Messages.menu_signInStatus_loading; @@ -73,7 +73,7 @@ private String getSignInStatusBasedOnAuthResult(AuthStatusResult authStatusResul } private void addSignInOrSignOutAction(MenuManager menuManager) { - if (authStatusManager.getAuthStatusResult().isSignedIn()) { + if (copilotStatusManager.getCopilotStatusResult().isSignedIn()) { ImageDescriptor signInIcon = UiUtils.buildImageDescriptorFromPngPath("/icons/signin.png"); MenuActionFactory.createMenuAction(menuManager, Messages.menu_signOutFromGitHub, signInIcon, handlerService, "com.microsoft.copilot.eclipse.commands.signOut", true); diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/SignInHandler.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/SignInHandler.java index 800a3334..5aa48346 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/SignInHandler.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/SignInHandler.java @@ -10,8 +10,8 @@ import org.eclipse.jface.dialogs.MessageDialog; import org.eclipse.swt.widgets.Shell; -import com.microsoft.copilot.eclipse.core.AuthStatusManager; import com.microsoft.copilot.eclipse.core.CopilotCore; +import com.microsoft.copilot.eclipse.core.CopilotStatusManager; import com.microsoft.copilot.eclipse.core.lsp.protocol.SignInInitiateResult; import com.microsoft.copilot.eclipse.ui.dialogs.SignInConfirmDialog; import com.microsoft.copilot.eclipse.ui.dialogs.SignInDialog; @@ -26,13 +26,13 @@ public class SignInHandler extends AbstractHandler { private static final long SIGNIN_TIMEOUT_MILLIS = 180000L; - private AuthStatusManager authStatusManager; + private CopilotStatusManager copilotStatusManager; /** * Initialize the Copilot Language Server for the SignInHandler. */ public SignInHandler() { - this.authStatusManager = CopilotCore.getPlugin().getAuthStatusManager(); + this.copilotStatusManager = CopilotCore.getPlugin().getCopilotStatusManager(); } @Override @@ -55,7 +55,7 @@ public Object execute(ExecutionEvent event) throws ExecutionException { } private SignInInitiateResult initiateSignIn() throws Exception { - return this.authStatusManager.signInInitiate(); + return this.copilotStatusManager.signInInitiate(); } private void showAlreadySignedInMessage(Shell shell) { diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/SignOutHandler.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/SignOutHandler.java index a0699ca7..19b8e189 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/SignOutHandler.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/SignOutHandler.java @@ -7,10 +7,9 @@ import org.eclipse.jface.dialogs.MessageDialog; import org.eclipse.swt.widgets.Shell; -import com.microsoft.copilot.eclipse.core.AuthStatusManager; import com.microsoft.copilot.eclipse.core.CopilotCore; -import com.microsoft.copilot.eclipse.core.lsp.CopilotLanguageServerConnection; -import com.microsoft.copilot.eclipse.core.lsp.protocol.AuthStatusResult; +import com.microsoft.copilot.eclipse.core.CopilotStatusManager; +import com.microsoft.copilot.eclipse.core.lsp.protocol.CopilotStatusResult; import com.microsoft.copilot.eclipse.ui.i18n.Messages; import com.microsoft.copilot.eclipse.ui.utils.SwtUtils; @@ -19,23 +18,23 @@ */ public class SignOutHandler extends AbstractHandler { - private AuthStatusManager authStatusManager; + private CopilotStatusManager copilotStatusManager; /** * Initialize the Copilot Language Server and Auth Status Manager for the SignOutHandler. */ public SignOutHandler() { - this.authStatusManager = CopilotCore.getPlugin().getAuthStatusManager(); + this.copilotStatusManager = CopilotCore.getPlugin().getCopilotStatusManager(); } @Override public Object execute(ExecutionEvent event) throws ExecutionException { Shell shell = SwtUtils.getShellFromEvent(event); try { - AuthStatusResult result = authStatusManager.signOut(); + CopilotStatusResult result = copilotStatusManager.signOut(); if (!result.isSignedIn()) { showSignOutMessage(shell); - authStatusManager.checkStatus(); + copilotStatusManager.checkStatus(); } } catch (Exception e) { handleSignOutException(shell, e); From b51657512f8dce65c11774539bc733a9b2bf07ec Mon Sep 17 00:00:00 2001 From: ethanyhou <149548697+ethanyhou@users.noreply.github.com> Date: Fri, 20 Dec 2024 10:31:18 +0800 Subject: [PATCH 028/690] fix - Update plugin.xml icon png name (#38) --- ...blue.png => github_copilot_error_blue.png} | Bin ...x.png => github_copilot_error_blue@2x.png} | Bin com.microsoft.copilot.eclipse.ui/plugin.xml | 40 +++++++++--------- .../eclipse/ui/dialogs/SignInDialog.java | 2 +- .../eclipse/ui/handlers/SignInHandler.java | 4 +- .../eclipse/ui/handlers/SignOutHandler.java | 2 +- .../copilot/eclipse/ui/i18n/Messages.java | 6 +-- .../eclipse/ui/i18n/messages.properties | 6 +-- 8 files changed, 30 insertions(+), 30 deletions(-) rename com.microsoft.copilot.eclipse.ui/icons/{gitHub_copilot_error_blue.png => github_copilot_error_blue.png} (100%) rename com.microsoft.copilot.eclipse.ui/icons/{gitHub_copilot_error_blue@2x.png => github_copilot_error_blue@2x.png} (100%) diff --git a/com.microsoft.copilot.eclipse.ui/icons/gitHub_copilot_error_blue.png b/com.microsoft.copilot.eclipse.ui/icons/github_copilot_error_blue.png similarity index 100% rename from com.microsoft.copilot.eclipse.ui/icons/gitHub_copilot_error_blue.png rename to com.microsoft.copilot.eclipse.ui/icons/github_copilot_error_blue.png diff --git a/com.microsoft.copilot.eclipse.ui/icons/gitHub_copilot_error_blue@2x.png b/com.microsoft.copilot.eclipse.ui/icons/github_copilot_error_blue@2x.png similarity index 100% rename from com.microsoft.copilot.eclipse.ui/icons/gitHub_copilot_error_blue@2x.png rename to com.microsoft.copilot.eclipse.ui/icons/github_copilot_error_blue@2x.png diff --git a/com.microsoft.copilot.eclipse.ui/plugin.xml b/com.microsoft.copilot.eclipse.ui/plugin.xml index 9ed13ce7..466def52 100644 --- a/com.microsoft.copilot.eclipse.ui/plugin.xml +++ b/com.microsoft.copilot.eclipse.ui/plugin.xml @@ -17,7 +17,7 @@ id="com.microsoft.copilot.eclipse.ui.statusBar"> @@ -28,17 +28,17 @@ - - - - - + id="com.microsoft.copilot.eclipse.commands.showStatusBarMenu" + name="%command.copilotForEclipsePlugin.name"> +
+ + + + @@ -60,14 +60,14 @@ class="com.microsoft.copilot.eclipse.ui.handlers.ShowStatusBarMenuHandler" commandId="com.microsoft.copilot.eclipse.commands.showStatusBarMenu"> - - - - + + + + diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/dialogs/SignInDialog.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/dialogs/SignInDialog.java index 38e100b3..7d869924 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/dialogs/SignInDialog.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/dialogs/SignInDialog.java @@ -66,7 +66,7 @@ private void createDeviceCodeSection(Composite composite) { private void createWebsiteSection(Composite composite) { Label websiteLabel = new Label(composite, SWT.NONE); - websiteLabel.setText(Messages.signInDialog_info_gitHubWebSitePrefix); + websiteLabel.setText(Messages.signInDialog_info_githubWebSitePrefix); Link websiteLink = new Link(composite, SWT.NONE); websiteLink.setText("" + this.signInInitiateResult.getVerificationUri() + ""); websiteLink.addSelectionListener(new SelectionAdapter() { diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/SignInHandler.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/SignInHandler.java index 5aa48346..358afaaf 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/SignInHandler.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/SignInHandler.java @@ -88,7 +88,7 @@ private void handleSignInConfirmation(Shell shell, SignInConfirmDialog signInCon } private void showSignInSuccessMessage(Shell shell) { - MessageDialog.openInformation(shell, Messages.signInHandler_msgDialog_gitHubCopilot, + MessageDialog.openInformation(shell, Messages.signInHandler_msgDialog_githubCopilot, Messages.signInHandler_msgDialog_signInSuccess); } @@ -98,7 +98,7 @@ private void showSignInFailMessage(Shell shell, IStatus status) { msg += ": " + status.getMessage(); } msg += ". "; - MessageDialog.openInformation(shell, Messages.signInHandler_msgDialog_gitHubCopilot, + MessageDialog.openInformation(shell, Messages.signInHandler_msgDialog_githubCopilot, msg + Messages.signInHandler_msgDialog_signInFailedTryAgain); } diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/SignOutHandler.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/SignOutHandler.java index 19b8e189..3c1c0971 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/SignOutHandler.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/SignOutHandler.java @@ -54,7 +54,7 @@ private void handleSignOutException(Shell shell, Exception e) { } private void showSignOutMessage(Shell shell) { - MessageDialog.openInformation(shell, Messages.signOutHandler_msgDialog_gitHubCopilot, + MessageDialog.openInformation(shell, Messages.signOutHandler_msgDialog_githubCopilot, Messages.signOutHandler_msgDialog_signOutSuccess); } diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/i18n/Messages.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/i18n/Messages.java index d17b897e..fcf7264b 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/i18n/Messages.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/i18n/Messages.java @@ -21,21 +21,21 @@ public final class Messages extends NLS { public static String signInDialog_button_copyOpen; public static String signInDialog_info_instructions; public static String signInDialog_info_deviceCodePrefix; - public static String signInDialog_info_gitHubWebSitePrefix; + public static String signInDialog_info_githubWebSitePrefix; public static String signInConfirmDialog_progress; public static String signInConfirmDialog_progressSuffix; public static String signInConfirmDialog_progressTimeout; public static String signInConfirmDialog_progressCanceled; public static String signInConfirmDialog_authResult_notSignedIn; public static String signInConfirmDialog_authResult_notAuthed; - public static String signInHandler_msgDialog_gitHubCopilot; + public static String signInHandler_msgDialog_githubCopilot; public static String signInHandler_msgDialog_title; public static String signInHandler_msgDialog_alreadySignedIn; public static String signInHandler_msgDialog_signInSuccess; public static String signInHandler_msgDialog_signInFailed; public static String signInHandler_msgDialog_signInFailedTryAgain; public static String signInHandler_msgDialog_signInFailedFailure; - public static String signOutHandler_msgDialog_gitHubCopilot; + public static String signOutHandler_msgDialog_githubCopilot; public static String signOutHandler_msgDialog_signOutSuccess; public static String signOutHandler_msgDialog_signOutFailed; public static String signOutHandler_msgDialog_signOutFailedFailure; diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/i18n/messages.properties b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/i18n/messages.properties index 50672d46..5a1d1015 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/i18n/messages.properties +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/i18n/messages.properties @@ -13,20 +13,20 @@ signInDialog_button_cancel=Cancel signInDialog_button_copyOpen=Copy Code and Open signInDialog_info_instructions=GitHub Copilot uses a GitHub account. Please enter the following code on the GitHub website to authorize your GitHub account with GitHub Copilot. signInDialog_info_deviceCodePrefix=Device code: -signInDialog_info_gitHubWebSitePrefix=GitHub website: +signInDialog_info_githubWebSitePrefix=GitHub website: signInConfirmDialog_progress=Waiting for GitHub Copilot authorization... signInConfirmDialog_progressTimeout=Authorization request failed: Process timed out. signInConfirmDialog_progressCanceled=Authorization request failed: process aborted. signInConfirmDialog_authResult_notSignedIn=Authorization request failed: Not signed in. signInConfirmDialog_authResult_notAuthed=Authorization request failed: Your subscription may be expired. -signInHandler_msgDialog_gitHubCopilot=GitHub Copilot +signInHandler_msgDialog_githubCopilot=GitHub Copilot signInHandler_msgDialog_title=Sign In to GitHub signInHandler_msgDialog_alreadySignedIn=User already signed in. signInHandler_msgDialog_signInSuccess=You have successfully signed in and authorized GitHub Copilot access to your GitHub account. signInHandler_msgDialog_signInFailed=Unable to sign in to GitHub Copilot at this time signInHandler_msgDialog_signInFailedTryAgain= Please try again to resume use of GitHub Copilot features. signInHandler_msgDialog_signInFailedFailure=Copilot Sign In Failure -signOutHandler_msgDialog_gitHubCopilot=GitHub Copilot +signOutHandler_msgDialog_githubCopilot=GitHub Copilot signOutHandler_msgDialog_signOutSuccess=You have successfully signed out from Copilot. signOutHandler_msgDialog_signOutFailed=Unable to sign out to GitHub Copilot at this time signOutHandler_msgDialog_signOutFailedFailure=Copilot Sign Out Failure \ No newline at end of file From 83e0f6a78004d35f39aa0ff6a014b3e81150e2f1 Mon Sep 17 00:00:00 2001 From: yanshudan <1397370237@qq.com> Date: Fri, 20 Dec 2024 13:31:52 +0800 Subject: [PATCH 029/690] feat - Add eclipse console logger (#30) --- .../META-INF/MANIFEST.MF | 3 +- .../copilot/eclipse/core/CopilotCore.java | 2 + .../copilot/eclipse/core/enums/LogLevel.java | 18 ++++ .../core/logger/CopilotForEclipseLogger.java | 79 ++++++++++++++++ .../handlers/EclipseConsoleHandler.java | 90 +++++++++++++++++++ .../core/lsp/LsStreamConnectionProvider.java | 4 +- launch/plugin_debug_configuration.launch | 1 + 7 files changed, 195 insertions(+), 2 deletions(-) create mode 100644 com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/enums/LogLevel.java create mode 100644 com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/logger/CopilotForEclipseLogger.java create mode 100644 com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/logger/handlers/EclipseConsoleHandler.java diff --git a/com.microsoft.copilot.eclipse.core/META-INF/MANIFEST.MF b/com.microsoft.copilot.eclipse.core/META-INF/MANIFEST.MF index bdcc5012..b65dbb85 100644 --- a/com.microsoft.copilot.eclipse.core/META-INF/MANIFEST.MF +++ b/com.microsoft.copilot.eclipse.core/META-INF/MANIFEST.MF @@ -19,4 +19,5 @@ Require-Bundle: org.eclipse.lsp4e;bundle-version="0.18.12", org.eclipse.lsp4j.jsonrpc;bundle-version="0.23.1", org.apache.commons.lang3;bundle-version="3.17.0", org.eclipse.jdt.annotation;bundle-version="2.3.0", - org.eclipse.jface.text + org.eclipse.jface.text, + com.google.gson;bundle-version="2.11.0" diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/CopilotCore.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/CopilotCore.java index 0b3501eb..5165f912 100644 --- a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/CopilotCore.java +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/CopilotCore.java @@ -11,6 +11,7 @@ import org.osgi.framework.BundleContext; import com.microsoft.copilot.eclipse.core.completion.CompletionProvider; +import com.microsoft.copilot.eclipse.core.logger.CopilotForEclipseLogger; import com.microsoft.copilot.eclipse.core.lsp.CopilotLanguageServerConnection; /** @@ -24,6 +25,7 @@ public class CopilotCore extends Plugin { private CompletionProvider completionProvider; private static CopilotCore COPILOT_CORE_PLUGIN = null; + public static final CopilotForEclipseLogger LOGGER = new CopilotForEclipseLogger(CopilotCore.class.getName()); /** * Creates the Copilot core plugin. The plugin is created automatically by the Eclipse framework. Clients must not diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/enums/LogLevel.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/enums/LogLevel.java new file mode 100644 index 00000000..f7de0772 --- /dev/null +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/enums/LogLevel.java @@ -0,0 +1,18 @@ +package com.microsoft.copilot.eclipse.core.enums; + +/** + * The event type enum. + */ +public enum LogLevel { + INFO("INFO"), WARNING("WARNING"), ERROR("ERROR"); + + private String value; + + LogLevel(String string) { + this.value = string; + } + + public String getValue() { + return this.value; + } +} diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/logger/CopilotForEclipseLogger.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/logger/CopilotForEclipseLogger.java new file mode 100644 index 00000000..c1751613 --- /dev/null +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/logger/CopilotForEclipseLogger.java @@ -0,0 +1,79 @@ +package com.microsoft.copilot.eclipse.core.logger; + +import java.util.logging.Level; +import java.util.logging.LogRecord; +import java.util.logging.Logger; + +import org.eclipse.core.runtime.Platform; +import org.osgi.framework.Bundle; +import org.osgi.framework.FrameworkUtil; + +import com.microsoft.copilot.eclipse.core.enums.LogLevel; +import com.microsoft.copilot.eclipse.core.logger.handlers.EclipseConsoleHandler; + +/** + * The logger for Copilot for Eclipse. + */ +public class CopilotForEclipseLogger { + //TODO: migrate to xml configuration + private Logger logger; + + /** + * Constructor. + * + * @param name the name of the logger + * + */ + public CopilotForEclipseLogger(String name) { + logger = Logger.getLogger(name); + setupLoggers(logger); + } + + /** + * Log the message. + * + * @param lvl the log level + * @param parameters the parameters + */ + public void log(LogLevel lvl, Object... parameters) { + Level level = map2Level(lvl); + LogRecord logRecord = new LogRecord(level, ""); + logRecord.setParameters(new Object[] { lvl, parameters }); + logger.log(logRecord); + } + + /** + * Set up the loggers. + * + * @param LOGGER the logger + */ + private static void setupLoggers(Logger logger) { + logger.setUseParentHandlers(false); + Bundle bundle = FrameworkUtil.getBundle(CopilotForEclipseLogger.class); + if (bundle == null) { + return; + } + EclipseConsoleHandler consoleHandler = new EclipseConsoleHandler(Platform.getLog(bundle)); + consoleHandler.setLevel(Level.ALL); + logger.addHandler(consoleHandler); + } + + /** + * Map the LogLevel to the Level. + * + * @param level the LogLevel + * @return the Level + */ + private Level map2Level(LogLevel level) { + switch (level) { + case INFO: + return Level.INFO; + case WARNING: + return Level.WARNING; + case ERROR: + return Level.SEVERE; + default: + return Level.INFO; + } + } +} \ No newline at end of file diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/logger/handlers/EclipseConsoleHandler.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/logger/handlers/EclipseConsoleHandler.java new file mode 100644 index 00000000..c0acb133 --- /dev/null +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/logger/handlers/EclipseConsoleHandler.java @@ -0,0 +1,90 @@ +package com.microsoft.copilot.eclipse.core.logger.handlers; + +import java.util.logging.Handler; +import java.util.logging.LogRecord; + +import com.google.gson.Gson; +import org.eclipse.core.runtime.ILog; +import org.eclipse.core.runtime.Status; + +import com.microsoft.copilot.eclipse.core.Constants; +import com.microsoft.copilot.eclipse.core.enums.LogLevel; + +/** + * The log appender to send info and error messages to the Eclipse console. + */ +public class EclipseConsoleHandler extends Handler { + private ILog logger; + + /** + * Constructor. + */ + public EclipseConsoleHandler(ILog logger) { + this.logger = logger; + } + + @Override + public void publish(LogRecord logRecord) { + if (logger == null) { + return; + } + Object[] property = logRecord.getParameters(); + if (property == null || property.length < 2) { + return; + } + if (!(property[0] instanceof LogLevel)) { + return; + } + LogLevel lvl = (LogLevel) property[0]; + Object[] params = (Object[]) property[1]; + // TODO: do we need to add objects in the log message? + int level = map2StatusLevel(lvl); + logger.log(new Status(level, Constants.PLUGIN_ID, getFormatedMessage(params), getThrowable(params))); + } + + private String getFormatedMessage(Object[] properties) { + String str = ""; + for (int i = 0; i < properties.length; i++) { + str += "argv" + i + " = "; + try { + str += new Gson().toJson(properties[i]); + } catch (Exception e) { + str += "exceptionInToJson"; + } + str += " ;"; + } + return str; + } + + private Throwable getThrowable(Object[] properties) { + for (int i = 1; i < properties.length; i++) { + if (properties[i] instanceof Throwable) { + return (Throwable) properties[i]; + } + } + return null; + } + + @Override + public void flush() { + // do nothing + } + + @Override + public void close() throws SecurityException { + // do nothing + } + + private int map2StatusLevel(LogLevel level) { + switch (level) { + case INFO: + return Status.INFO; + case WARNING: + return Status.WARNING; + case ERROR: + return Status.ERROR; + default: + return Status.INFO; + } + } +} \ No newline at end of file diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/LsStreamConnectionProvider.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/LsStreamConnectionProvider.java index 48ba83c5..89e79fee 100644 --- a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/LsStreamConnectionProvider.java +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/LsStreamConnectionProvider.java @@ -43,7 +43,9 @@ public Object getInitializationOptions(@Nullable URI rootUri) { @Override public void start() throws IOException { - Path binary = findBinary(); + // load lsp binary + // call normalize to remove any relative path components and avoid "FILE_PATH_TOO_LONG" error + Path binary = findBinary().normalize(); if (binary == null) { throw new IOException("Could not find the language server binary"); } diff --git a/launch/plugin_debug_configuration.launch b/launch/plugin_debug_configuration.launch index fec96a04..f937ebb3 100644 --- a/launch/plugin_debug_configuration.launch +++ b/launch/plugin_debug_configuration.launch @@ -151,6 +151,7 @@ + From 52816a1a66b2bfcd1324c68748a2f57df9015306 Mon Sep 17 00:00:00 2001 From: Sheng Chen Date: Fri, 20 Dec 2024 16:03:22 +0800 Subject: [PATCH 030/690] fix - Check the status before triggering completion (#42) --- .../core/CopilotStatusManagerTests.java | 9 ++++----- .../completion/CompletionProviderTests.java | 20 ++++++++++++++++++- .../.settings/org.eclipse.jdt.core.prefs | 2 +- .../copilot/eclipse/core/CopilotCore.java | 2 +- .../eclipse/core/CopilotStatusManager.java | 20 ++++++++++++------- .../core/completion/CompletionProvider.java | 11 ++++++++-- .../.settings/org.eclipse.jdt.core.prefs | 2 +- .../ui/completion/CompletionManager.java | 4 ++-- .../ui/handlers/ShowStatusBarMenuHandler.java | 14 +++++++------ 9 files changed, 58 insertions(+), 26 deletions(-) diff --git a/com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/CopilotStatusManagerTests.java b/com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/CopilotStatusManagerTests.java index e5c33e01..2286368d 100644 --- a/com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/CopilotStatusManagerTests.java +++ b/com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/CopilotStatusManagerTests.java @@ -35,7 +35,7 @@ void testCopilotStatusResultOnSuccess() { copilotStatusManager.checkStatus(); - assertEquals(CopilotStatusResult.OK, copilotStatusManager.getCopilotStatusResult().getStatus()); + assertEquals(CopilotStatusResult.OK, copilotStatusManager.getCopilotStatus()); } @Test @@ -53,13 +53,12 @@ void testCheckStatusLoadingWithDelay() throws InterruptedException { copilotStatusManager.checkStatus(); // Assert initial status is LOADING - assertEquals(CopilotStatusResult.LOADING, copilotStatusManager.getCopilotStatusResult().getStatus()); + assertEquals(CopilotStatusResult.LOADING, copilotStatusManager.getCopilotStatus()); future.complete(expectedResult); // Assert final status is OK - assertEquals(CopilotStatusResult.OK, copilotStatusManager.getCopilotStatusResult().getStatus()); - assertEquals(mockedUser, copilotStatusManager.getCopilotStatusResult().getUser()); + assertEquals(CopilotStatusResult.OK, copilotStatusManager.getCopilotStatus()); } @Test @@ -71,7 +70,7 @@ void testCheckStatusError() { copilotStatusManager.checkStatus(); - assertEquals(CopilotStatusResult.ERROR, copilotStatusManager.getCopilotStatusResult().getStatus()); + assertEquals(CopilotStatusResult.ERROR, copilotStatusManager.getCopilotStatus()); } } diff --git a/com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/completion/CompletionProviderTests.java b/com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/completion/CompletionProviderTests.java index e1dadf29..49243404 100644 --- a/com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/completion/CompletionProviderTests.java +++ b/com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/completion/CompletionProviderTests.java @@ -2,6 +2,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -19,9 +20,11 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import com.microsoft.copilot.eclipse.core.CopilotStatusManager; import com.microsoft.copilot.eclipse.core.lsp.CopilotLanguageServerConnection; import com.microsoft.copilot.eclipse.core.lsp.protocol.CompletionItem; import com.microsoft.copilot.eclipse.core.lsp.protocol.CompletionResult; +import com.microsoft.copilot.eclipse.core.lsp.protocol.CopilotStatusResult; @ExtendWith(MockitoExtension.class) class CompletionProviderTests { @@ -29,15 +32,19 @@ class CompletionProviderTests { @Mock private CopilotLanguageServerConnection mockLsConnection; + @Mock + private CopilotStatusManager mockStatusManager; + @Mock private CompletionListener mockListener; @Test void testShouldNotifyListenersOnCompletion() throws OperationCanceledException, InterruptedException { + when(mockStatusManager.getCopilotStatus()).thenReturn(CopilotStatusResult.OK); when(mockLsConnection.getCompletions(any())) .thenReturn(CompletableFuture.completedFuture(new CompletionResult(List.of(mock(CompletionItem.class))))); - CompletionProvider completionProvider = new CompletionProvider(mockLsConnection); + CompletionProvider completionProvider = new CompletionProvider(mockLsConnection, mockStatusManager); completionProvider.addCompletionListener(mockListener); Position position = new Position(0, 0); completionProvider.triggerCompletion("file://test.java", position, 1); @@ -46,4 +53,15 @@ void testShouldNotifyListenersOnCompletion() throws OperationCanceledException, verify(mockLsConnection, times(1)).getCompletions(any()); } + @Test + void testShouldNotTriggerCompletionWhenNotSignedIn() throws OperationCanceledException, InterruptedException { + when(mockStatusManager.getCopilotStatus()).thenReturn(CopilotStatusResult.NOT_SIGNED_IN); + + CompletionProvider completionProvider = new CompletionProvider(mockLsConnection, mockStatusManager); + completionProvider.addCompletionListener(mockListener); + Position position = new Position(0, 0); + completionProvider.triggerCompletion("file://test.java", position, 1); + verify(mockLsConnection, never()).getCompletions(any()); + } + } diff --git a/com.microsoft.copilot.eclipse.core/.settings/org.eclipse.jdt.core.prefs b/com.microsoft.copilot.eclipse.core/.settings/org.eclipse.jdt.core.prefs index aa5854b6..21eccc09 100644 --- a/com.microsoft.copilot.eclipse.core/.settings/org.eclipse.jdt.core.prefs +++ b/com.microsoft.copilot.eclipse.core/.settings/org.eclipse.jdt.core.prefs @@ -134,7 +134,7 @@ org.eclipse.jdt.core.formatter.indent_empty_lines=false org.eclipse.jdt.core.formatter.indent_statements_compare_to_block=true org.eclipse.jdt.core.formatter.indent_statements_compare_to_body=true org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_cases=true -org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_switch=false +org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_switch=true org.eclipse.jdt.core.formatter.indentation.size=2 org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_enum_constant=insert org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_field=insert diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/CopilotCore.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/CopilotCore.java index 5165f912..844e0c01 100644 --- a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/CopilotCore.java +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/CopilotCore.java @@ -65,8 +65,8 @@ void init() { LanguageServerWrapper wrapper = LanguageServiceAccessor.startLanguageServer(serverDef); this.copilotLanguageServer = new CopilotLanguageServerConnection(wrapper); - this.completionProvider = new CompletionProvider(this.copilotLanguageServer); this.copilotStatusManager = new CopilotStatusManager(this.copilotLanguageServer); + this.completionProvider = new CompletionProvider(this.copilotLanguageServer, copilotStatusManager); this.copilotStatusManager.checkStatus(); }; diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/CopilotStatusManager.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/CopilotStatusManager.java index 6693a750..06c5c5cf 100644 --- a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/CopilotStatusManager.java +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/CopilotStatusManager.java @@ -18,7 +18,7 @@ public class CopilotStatusManager { private CopilotStatusResult copilotStatusResult; private static final int CHECK_STATUS_TIMEOUT_MILLIS = 3000; - + /** * Constructor for the CopilotStatusManager. * @@ -32,7 +32,7 @@ public CopilotStatusManager(CopilotLanguageServerConnection connection) { /** * Initiate the sign in process. - + * * @throws ExecutionException if the sign in initiate process fails due to an execution error * @throws InterruptedException if the sign in initiate process is interrupted */ @@ -46,7 +46,7 @@ public SignInInitiateResult signInInitiate() throws InterruptedException, Execut /** * Confirm the sign in process. - + * * @throws ExecutionException if the sign in process fails due to an execution error * @throws InterruptedException if the sign in process is interrupted */ @@ -61,7 +61,7 @@ public CopilotStatusResult signInConfirm(String userCode) throws InterruptedExce /** * Sign out from the GitHub Copilot. - + * * @throws ExecutionException if the sign out process fails due to an execution error * @throws InterruptedException if the sign out process is interrupted */ @@ -84,12 +84,18 @@ public void checkStatus() { }).exceptionally(ex -> { // TODO: log & send telemetry this.copilotStatusResult.setStatus(CopilotStatusResult.ERROR); - + return null; }); } - public CopilotStatusResult getCopilotStatusResult() { - return this.copilotStatusResult; + /** + * Get the current status of the copilot. + */ + public String getCopilotStatus() { + if (this.copilotStatusResult == null) { + return CopilotStatusResult.LOADING; + } + return this.copilotStatusResult.getStatus(); } } diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/completion/CompletionProvider.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/completion/CompletionProvider.java index 5b362169..4ee87467 100644 --- a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/completion/CompletionProvider.java +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/completion/CompletionProvider.java @@ -1,6 +1,7 @@ package com.microsoft.copilot.eclipse.core.completion; import java.util.LinkedHashSet; +import java.util.Objects; import java.util.Set; import org.eclipse.core.runtime.IStatus; @@ -8,10 +9,12 @@ import org.eclipse.core.runtime.jobs.IJobChangeListener; import org.eclipse.lsp4j.Position; +import com.microsoft.copilot.eclipse.core.CopilotStatusManager; import com.microsoft.copilot.eclipse.core.lsp.CopilotLanguageServerConnection; import com.microsoft.copilot.eclipse.core.lsp.protocol.CompletionDocument; import com.microsoft.copilot.eclipse.core.lsp.protocol.CompletionParams; import com.microsoft.copilot.eclipse.core.lsp.protocol.CompletionResult; +import com.microsoft.copilot.eclipse.core.lsp.protocol.CopilotStatusResult; /** * Provider for inline completion. @@ -19,13 +22,14 @@ public class CompletionProvider implements IJobChangeListener { private CompletionJob completionJob; - private Set completionListeners; + private CopilotStatusManager statusManager; /** * Creates a new completion provider. */ - public CompletionProvider(CopilotLanguageServerConnection lsConnection) { + public CompletionProvider(CopilotLanguageServerConnection lsConnection, CopilotStatusManager statusManager) { + this.statusManager = statusManager; this.completionJob = new CompletionJob(lsConnection); this.completionJob.addJobChangeListener(this); this.completionListeners = new LinkedHashSet<>(); @@ -39,6 +43,9 @@ public CompletionProvider(CopilotLanguageServerConnection lsConnection) { * @param documentVersion the version of the document. */ public void triggerCompletion(String uriString, Position position, int documentVersion) { + if (!Objects.equals(statusManager.getCopilotStatus(), CopilotStatusResult.OK)) { + return; + } this.completionJob.cancel(); this.completionJob.setCompletionParams(null); CompletionDocument completionDoc = new CompletionDocument(uriString, position); diff --git a/com.microsoft.copilot.eclipse.ui/.settings/org.eclipse.jdt.core.prefs b/com.microsoft.copilot.eclipse.ui/.settings/org.eclipse.jdt.core.prefs index aa5854b6..21eccc09 100644 --- a/com.microsoft.copilot.eclipse.ui/.settings/org.eclipse.jdt.core.prefs +++ b/com.microsoft.copilot.eclipse.ui/.settings/org.eclipse.jdt.core.prefs @@ -134,7 +134,7 @@ org.eclipse.jdt.core.formatter.indent_empty_lines=false org.eclipse.jdt.core.formatter.indent_statements_compare_to_block=true org.eclipse.jdt.core.formatter.indent_statements_compare_to_body=true org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_cases=true -org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_switch=false +org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_switch=true org.eclipse.jdt.core.formatter.indentation.size=2 org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_enum_constant=insert org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_field=insert diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionManager.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionManager.java index e2e6d787..6c812660 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionManager.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionManager.java @@ -142,10 +142,10 @@ public void paintControl(PaintEvent e) { String remainingLines = this.completions.getRemainingLines(); if (StringUtils.isNotBlank(remainingLines)) { int lineHeight = styledText.getLineHeight(); - int fontHeightt = gc.getFontMetrics().getHeight(); + int fontHeight = gc.getFontMetrics().getHeight(); int x = styledText.getLeftMargin(); Point offsetLocation = styledText.getLocationAtOffset(widgetOffset); - int y = offsetLocation.y + lineHeight * 2 - fontHeightt; + int y = offsetLocation.y + lineHeight * 2 - fontHeight; gc.drawText(remainingLines, x, y, true); } diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/ShowStatusBarMenuHandler.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/ShowStatusBarMenuHandler.java index 23461903..7382e60a 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/ShowStatusBarMenuHandler.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/ShowStatusBarMenuHandler.java @@ -1,5 +1,7 @@ package com.microsoft.copilot.eclipse.ui.handlers; +import java.util.Objects; + import org.eclipse.core.commands.AbstractHandler; import org.eclipse.core.commands.ExecutionEvent; import org.eclipse.core.commands.ExecutionException; @@ -34,8 +36,8 @@ public Object execute(ExecutionEvent event) throws ExecutionException { MenuManager menuManager = new MenuManager(); addStatusAction(menuManager); - - if (!copilotStatusManager.getCopilotStatusResult().isLoading()) { + + if (!Objects.equals(copilotStatusManager.getCopilotStatus(), CopilotStatusResult.LOADING)) { menuManager.add(new Separator()); addSignInOrSignOutAction(menuManager); } @@ -47,14 +49,14 @@ public Object execute(ExecutionEvent event) throws ExecutionException { } private void addStatusAction(MenuManager menuManager) { - String signInStatus = getSignInStatusBasedOnAuthResult(copilotStatusManager.getCopilotStatusResult()); + String signInStatus = getSignInStatusBasedOnAuthResult(copilotStatusManager.getCopilotStatus()); String signInStatusTitle = Messages.menu_signInStatus + ": " + signInStatus; MenuActionFactory.createMenuAction(menuManager, signInStatusTitle, handlerService, signInStatus, false); } - private String getSignInStatusBasedOnAuthResult(CopilotStatusResult copilotStatusResult) { - switch (copilotStatusResult.getStatus()) { + private String getSignInStatusBasedOnAuthResult(String copilotStatus) { + switch (copilotStatus) { case CopilotStatusResult.OK: return Messages.menu_signInStatus_ready; case CopilotStatusResult.ERROR: @@ -73,7 +75,7 @@ private String getSignInStatusBasedOnAuthResult(CopilotStatusResult copilotStatu } private void addSignInOrSignOutAction(MenuManager menuManager) { - if (copilotStatusManager.getCopilotStatusResult().isSignedIn()) { + if (Objects.equals(copilotStatusManager.getCopilotStatus(), CopilotStatusResult.OK)) { ImageDescriptor signInIcon = UiUtils.buildImageDescriptorFromPngPath("/icons/signin.png"); MenuActionFactory.createMenuAction(menuManager, Messages.menu_signOutFromGitHub, signInIcon, handlerService, "com.microsoft.copilot.eclipse.commands.signOut", true); From c014e577ef1b1fa381463abaa1e51b11ab9cc833 Mon Sep 17 00:00:00 2001 From: yanshudan <1397370237@qq.com> Date: Mon, 23 Dec 2024 10:58:57 +0800 Subject: [PATCH 031/690] feat - Show logs in platform log view (#43) Co-authored-by: Huiquan Jiang --- .../META-INF/MANIFEST.MF | 1 + .../copilot/eclipse/core/CopilotCore.java | 6 ++++-- .../copilot/eclipse/core/CopilotStatusManager.java | 3 ++- .../core/completion/CompletionCollection.java | 7 +++++-- .../eclipse/core/completion/CompletionJob.java | 4 +++- .../core/completion/CompletionProvider.java | 3 ++- .../core/logger/CopilotForEclipseLogger.java | 1 - .../eclipse/core/{enums => logger}/LogLevel.java | 2 +- .../logger/handlers/EclipseConsoleHandler.java | 3 +-- .../core/lsp/LsStreamConnectionProvider.java | 3 ++- .../microsoft/copilot/eclipse/ui/CopilotUi.java | 10 +++++++--- .../eclipse/ui/completion/CompletionHandler.java | 14 ++++++++------ .../eclipse/ui/completion/CompletionManager.java | 4 ++-- .../eclipse/ui/dialogs/SignInConfirmDialog.java | 6 ++++-- .../ui/handlers/ShowStatusBarMenuHandler.java | 4 +++- .../copilot/eclipse/ui/handlers/SignInHandler.java | 5 +++-- .../eclipse/ui/handlers/SignOutHandler.java | 5 +++-- .../copilot/eclipse/ui/utils/UiUtils.java | 6 ++++-- 18 files changed, 55 insertions(+), 32 deletions(-) rename com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/{enums => logger}/LogLevel.java (83%) diff --git a/com.microsoft.copilot.eclipse.core/META-INF/MANIFEST.MF b/com.microsoft.copilot.eclipse.core/META-INF/MANIFEST.MF index b65dbb85..fccf9829 100644 --- a/com.microsoft.copilot.eclipse.core/META-INF/MANIFEST.MF +++ b/com.microsoft.copilot.eclipse.core/META-INF/MANIFEST.MF @@ -5,6 +5,7 @@ Bundle-SymbolicName: com.microsoft.copilot.eclipse.core;singleton:=true Bundle-Version: 0.1.0.qualifier Export-Package: com.microsoft.copilot.eclipse.core, com.microsoft.copilot.eclipse.core.completion, + com.microsoft.copilot.eclipse.core.logger, com.microsoft.copilot.eclipse.core.lsp, com.microsoft.copilot.eclipse.core.lsp.protocol, com.microsoft.copilot.eclipse.core.utils diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/CopilotCore.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/CopilotCore.java index 844e0c01..bda5ecf2 100644 --- a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/CopilotCore.java +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/CopilotCore.java @@ -12,6 +12,7 @@ import com.microsoft.copilot.eclipse.core.completion.CompletionProvider; import com.microsoft.copilot.eclipse.core.logger.CopilotForEclipseLogger; +import com.microsoft.copilot.eclipse.core.logger.LogLevel; import com.microsoft.copilot.eclipse.core.lsp.CopilotLanguageServerConnection; /** @@ -58,9 +59,10 @@ void init() { LanguageServersRegistry.LanguageServerDefinition serverDef = LanguageServersRegistry.getInstance() .getDefinition(CopilotLanguageServerConnection.SERVER_ID); if (serverDef == null) { - // TODO: log & send telemetry - throw new IllegalStateException( + var ex = new IllegalStateException( "Language server definition not found for " + CopilotLanguageServerConnection.SERVER_ID); + CopilotCore.LOGGER.log(LogLevel.ERROR, ex); + throw ex; } LanguageServerWrapper wrapper = LanguageServiceAccessor.startLanguageServer(serverDef); diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/CopilotStatusManager.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/CopilotStatusManager.java index 06c5c5cf..d1936eb7 100644 --- a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/CopilotStatusManager.java +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/CopilotStatusManager.java @@ -4,6 +4,7 @@ import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; +import com.microsoft.copilot.eclipse.core.logger.LogLevel; import com.microsoft.copilot.eclipse.core.lsp.CopilotLanguageServerConnection; import com.microsoft.copilot.eclipse.core.lsp.protocol.CopilotStatusResult; import com.microsoft.copilot.eclipse.core.lsp.protocol.SignInInitiateResult; @@ -82,7 +83,7 @@ public void checkStatus() { statusFuture.orTimeout(CHECK_STATUS_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS).thenAccept(result -> { this.copilotStatusResult = result; }).exceptionally(ex -> { - // TODO: log & send telemetry + CopilotCore.LOGGER.log(LogLevel.ERROR, ex); this.copilotStatusResult.setStatus(CopilotStatusResult.ERROR); return null; diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/completion/CompletionCollection.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/completion/CompletionCollection.java index e3a39533..bc1ad2f6 100644 --- a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/completion/CompletionCollection.java +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/completion/CompletionCollection.java @@ -4,6 +4,8 @@ import org.eclipse.jdt.annotation.NonNull; +import com.microsoft.copilot.eclipse.core.CopilotCore; +import com.microsoft.copilot.eclipse.core.logger.LogLevel; import com.microsoft.copilot.eclipse.core.lsp.protocol.CompletionItem; /** @@ -20,8 +22,9 @@ public class CompletionCollection { */ public CompletionCollection(@NonNull List completions, String uriString) { if (completions == null || completions.isEmpty()) { - throw new IllegalArgumentException("completions cannot be null or empty"); - // TODO: log & send telemetry + var ex = new IllegalArgumentException("completions cannot be null or empty"); + CopilotCore.LOGGER.log(LogLevel.ERROR, ex); + throw ex; } this.completions = completions; this.uriString = uriString; diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/completion/CompletionJob.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/completion/CompletionJob.java index 5b1972c0..c5933268 100644 --- a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/completion/CompletionJob.java +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/completion/CompletionJob.java @@ -9,6 +9,8 @@ import org.eclipse.core.runtime.jobs.Job; import com.microsoft.copilot.eclipse.core.Constants; +import com.microsoft.copilot.eclipse.core.CopilotCore; +import com.microsoft.copilot.eclipse.core.logger.LogLevel; import com.microsoft.copilot.eclipse.core.lsp.CopilotLanguageServerConnection; import com.microsoft.copilot.eclipse.core.lsp.protocol.CompletionParams; import com.microsoft.copilot.eclipse.core.lsp.protocol.CompletionResult; @@ -56,7 +58,7 @@ protected IStatus run(IProgressMonitor monitor) { } catch (InterruptedException e) { return Status.CANCEL_STATUS; } catch (ExecutionException e) { - // TODO: log & send telemetry + CopilotCore.LOGGER.log(LogLevel.ERROR, e); return new Status(IStatus.ERROR, Constants.PLUGIN_ID, e.getMessage(), e); } if (monitor.isCanceled()) { diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/completion/CompletionProvider.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/completion/CompletionProvider.java index 4ee87467..38082366 100644 --- a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/completion/CompletionProvider.java +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/completion/CompletionProvider.java @@ -9,7 +9,9 @@ import org.eclipse.core.runtime.jobs.IJobChangeListener; import org.eclipse.lsp4j.Position; +import com.microsoft.copilot.eclipse.core.CopilotCore; import com.microsoft.copilot.eclipse.core.CopilotStatusManager; +import com.microsoft.copilot.eclipse.core.logger.LogLevel; import com.microsoft.copilot.eclipse.core.lsp.CopilotLanguageServerConnection; import com.microsoft.copilot.eclipse.core.lsp.protocol.CompletionDocument; import com.microsoft.copilot.eclipse.core.lsp.protocol.CompletionParams; @@ -91,7 +93,6 @@ public void done(IJobChangeEvent event) { IStatus jobStatus = this.completionJob.getResult(); if (jobStatus != null && !jobStatus.isOK()) { return; - // TODO: log & send telemetry } CompletionResult result = this.completionJob.getCompletionResult(); if (result == null || result.getCompletions() == null || result.getCompletions().isEmpty()) { diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/logger/CopilotForEclipseLogger.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/logger/CopilotForEclipseLogger.java index c1751613..44ef6738 100644 --- a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/logger/CopilotForEclipseLogger.java +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/logger/CopilotForEclipseLogger.java @@ -8,7 +8,6 @@ import org.osgi.framework.Bundle; import org.osgi.framework.FrameworkUtil; -import com.microsoft.copilot.eclipse.core.enums.LogLevel; import com.microsoft.copilot.eclipse.core.logger.handlers.EclipseConsoleHandler; /** diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/enums/LogLevel.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/logger/LogLevel.java similarity index 83% rename from com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/enums/LogLevel.java rename to com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/logger/LogLevel.java index f7de0772..ba279c69 100644 --- a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/enums/LogLevel.java +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/logger/LogLevel.java @@ -1,4 +1,4 @@ -package com.microsoft.copilot.eclipse.core.enums; +package com.microsoft.copilot.eclipse.core.logger; /** * The event type enum. diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/logger/handlers/EclipseConsoleHandler.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/logger/handlers/EclipseConsoleHandler.java index c0acb133..07aa0e4e 100644 --- a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/logger/handlers/EclipseConsoleHandler.java +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/logger/handlers/EclipseConsoleHandler.java @@ -8,7 +8,7 @@ import org.eclipse.core.runtime.Status; import com.microsoft.copilot.eclipse.core.Constants; -import com.microsoft.copilot.eclipse.core.enums.LogLevel; +import com.microsoft.copilot.eclipse.core.logger.LogLevel; /** * The log appender to send info and error messages to the Eclipse console. @@ -37,7 +37,6 @@ public void publish(LogRecord logRecord) { } LogLevel lvl = (LogLevel) property[0]; Object[] params = (Object[]) property[1]; - // TODO: do we need to add objects in the log message? int level = map2StatusLevel(lvl); logger.log(new Status(level, Constants.PLUGIN_ID, getFormatedMessage(params), getThrowable(params))); } diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/LsStreamConnectionProvider.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/LsStreamConnectionProvider.java index 89e79fee..92ce2738 100644 --- a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/LsStreamConnectionProvider.java +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/LsStreamConnectionProvider.java @@ -17,6 +17,7 @@ import org.osgi.framework.Bundle; import com.microsoft.copilot.eclipse.core.CopilotCore; +import com.microsoft.copilot.eclipse.core.logger.LogLevel; import com.microsoft.copilot.eclipse.core.lsp.protocol.CopilotCapabilities; import com.microsoft.copilot.eclipse.core.lsp.protocol.InitializationOptions; import com.microsoft.copilot.eclipse.core.lsp.protocol.NameAndVersion; @@ -99,7 +100,7 @@ public void start() throws IOException { try { return URIUtil.toFile(URIUtil.toURI(FileLocator.toFileURL(url))).toPath(); } catch (URISyntaxException | IOException e) { - // TODO: Log exception via telemetry. + CopilotCore.LOGGER.log(LogLevel.ERROR, e); return null; } } diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/CopilotUi.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/CopilotUi.java index cda7ecd1..b5dc6d23 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/CopilotUi.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/CopilotUi.java @@ -9,6 +9,8 @@ import com.microsoft.copilot.eclipse.core.CopilotCore; import com.microsoft.copilot.eclipse.core.completion.CompletionListener; import com.microsoft.copilot.eclipse.core.completion.CompletionProvider; +import com.microsoft.copilot.eclipse.core.logger.CopilotForEclipseLogger; +import com.microsoft.copilot.eclipse.core.logger.LogLevel; import com.microsoft.copilot.eclipse.core.lsp.CopilotLanguageServerConnection; import com.microsoft.copilot.eclipse.ui.completion.CompletionStatusListener; import com.microsoft.copilot.eclipse.ui.completion.EditorLifecycleListener; @@ -26,6 +28,7 @@ public class CopilotUi extends Plugin { private CompletionStatusListener completionStatusListener; private EditorLifecycleListener editorLifecycleListener; private EditorsManager editorsManager; + public static final CopilotForEclipseLogger LOGGER = new CopilotForEclipseLogger(CopilotCore.class.getName()); /** * Creates the Copilot ui plugin. The plugin is created automatically by the Eclipse framework. Clients must not call @@ -53,8 +56,9 @@ public void start(BundleContext context) throws Exception { Thread.sleep(1000); } if (connection == null) { - // TODO: log & send telemetry - throw new IllegalStateException("Copilot language server is not ready."); + var ex = new IllegalStateException("Failed to start copilot language server."); + LOGGER.log(LogLevel.ERROR, ex); + throw ex; } this.editorsManager = new EditorsManager(connection, CopilotCore.getPlugin().getCompletionProvider()); @@ -105,7 +109,7 @@ private void unregisterPartListener() { window.getPartService().removePartListener(this.editorLifecycleListener); } } - + private void unregisterCompletionListener() { CopilotCore.getPlugin().getCompletionProvider().removeCompletionListener(this.completionStatusListener); } diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionHandler.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionHandler.java index 9eac539e..8717d56d 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionHandler.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionHandler.java @@ -19,7 +19,9 @@ import com.microsoft.copilot.eclipse.core.completion.CompletionCollection; import com.microsoft.copilot.eclipse.core.completion.CompletionProvider; +import com.microsoft.copilot.eclipse.core.logger.LogLevel; import com.microsoft.copilot.eclipse.core.lsp.CopilotLanguageServerConnection; +import com.microsoft.copilot.eclipse.ui.CopilotUi; import com.microsoft.copilot.eclipse.ui.utils.SwtUtils; import com.microsoft.copilot.eclipse.ui.utils.UiUtils; @@ -50,23 +52,23 @@ public CompletionHandler(CopilotLanguageServerConnection lsConnection, Completio // if the text viewer is null, we will not register listeners. // the side effect is that the completion will not be triggered for this editor. if (textViewer == null) { - // TODO: log & send telemetry + CopilotUi.LOGGER.log(LogLevel.INFO, "Text viewer is null for editor: " + editor.getTitle()); return; } this.document = LSPEclipseUtils.getDocument(editor); if (this.document == null) { - // TODO: log & send telemetry + CopilotUi.LOGGER.log(LogLevel.INFO, "Document is null for editor: " + editor.getTitle()); return; } this.documentUri = LSPEclipseUtils.toUri(document); if (this.documentUri == null) { - // TODO: log & send telemetry + CopilotUi.LOGGER.log(LogLevel.INFO, "Document URI is null for editor: " + editor.getTitle()); return; } try { lsConnection.connectDocument(this.document); } catch (IOException e) { - // TODO: log & send telemetry + CopilotUi.LOGGER.log(LogLevel.ERROR, e); return; } this.documentVersion = -1; @@ -100,7 +102,7 @@ public void acceptFullSuggestion() { this.completionManager.acceptSuggestion(); this.document.removePosition(this.triggerPosition); } catch (BadLocationException e) { - // TODO: log & send telemetry + CopilotUi.LOGGER.log(LogLevel.ERROR, e); return; } this.clearCompletionRendering(); @@ -187,7 +189,7 @@ public void dispose() { try { this.document.removePositionCategory(this.getCategory()); } catch (BadPositionCategoryException e) { - // TODO: log & send telemetry + CopilotUi.LOGGER.log(LogLevel.ERROR, e); } this.document.removePositionUpdater(this.positionUpdater); if (this.textViewer != null) { diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionManager.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionManager.java index 6c812660..a2bf110b 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionManager.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionManager.java @@ -21,6 +21,7 @@ import com.microsoft.copilot.eclipse.core.completion.CompletionCollection; import com.microsoft.copilot.eclipse.core.completion.CompletionListener; import com.microsoft.copilot.eclipse.core.completion.CompletionProvider; +import com.microsoft.copilot.eclipse.core.logger.LogLevel; import com.microsoft.copilot.eclipse.core.lsp.CopilotLanguageServerConnection; import com.microsoft.copilot.eclipse.core.lsp.protocol.CompletionItem; import com.microsoft.copilot.eclipse.core.lsp.protocol.NotifyShownParams; @@ -77,8 +78,7 @@ public void triggerCompletion(Position position, int documentVersion) { this.provider.triggerCompletion(documentUri.toASCIIString(), LSPEclipseUtils.toPosition(position.getOffset(), this.document), documentVersion); } catch (BadLocationException e) { - CopilotUi.getPlugin().getLog().info("triggerCompletion BadLocationException e 77"); - // TODO log & send telemetry + CopilotUi.LOGGER.log(LogLevel.ERROR, e); } } diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/dialogs/SignInConfirmDialog.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/dialogs/SignInConfirmDialog.java index 417ef6d0..c5558fc1 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/dialogs/SignInConfirmDialog.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/dialogs/SignInConfirmDialog.java @@ -12,7 +12,9 @@ import org.eclipse.swt.widgets.Shell; import com.microsoft.copilot.eclipse.core.CopilotCore; +import com.microsoft.copilot.eclipse.core.logger.LogLevel; import com.microsoft.copilot.eclipse.core.lsp.protocol.CopilotStatusResult; +import com.microsoft.copilot.eclipse.ui.CopilotUi; import com.microsoft.copilot.eclipse.ui.i18n.Messages; /** @@ -54,7 +56,7 @@ public void run() { try { this.run(true, true, task); } catch (Exception e) { - // TODO: log & send telemetry + CopilotUi.LOGGER.log(LogLevel.ERROR, e); } } @@ -75,7 +77,7 @@ public void run(IProgressMonitor monitor) throws InvocationTargetException, Inte try { return CopilotCore.getPlugin().getCopilotStatusManager().signInConfirm(userCode); } catch (Exception e) { - // TODO: log & send telemetry + CopilotUi.LOGGER.log(LogLevel.ERROR, e); return null; } }); diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/ShowStatusBarMenuHandler.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/ShowStatusBarMenuHandler.java index 7382e60a..7753b768 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/ShowStatusBarMenuHandler.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/ShowStatusBarMenuHandler.java @@ -17,7 +17,9 @@ import com.microsoft.copilot.eclipse.core.CopilotCore; import com.microsoft.copilot.eclipse.core.CopilotStatusManager; +import com.microsoft.copilot.eclipse.core.logger.LogLevel; import com.microsoft.copilot.eclipse.core.lsp.protocol.CopilotStatusResult; +import com.microsoft.copilot.eclipse.ui.CopilotUi; import com.microsoft.copilot.eclipse.ui.i18n.Messages; import com.microsoft.copilot.eclipse.ui.utils.UiUtils; @@ -95,7 +97,7 @@ public void run() { try { handlerService.executeCommand(commandId, null); } catch (Exception e) { - // TODO: log & send telemetry + CopilotUi.LOGGER.log(LogLevel.ERROR, e); } } }; diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/SignInHandler.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/SignInHandler.java index 358afaaf..d3c18e50 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/SignInHandler.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/SignInHandler.java @@ -12,7 +12,9 @@ import com.microsoft.copilot.eclipse.core.CopilotCore; import com.microsoft.copilot.eclipse.core.CopilotStatusManager; +import com.microsoft.copilot.eclipse.core.logger.LogLevel; import com.microsoft.copilot.eclipse.core.lsp.protocol.SignInInitiateResult; +import com.microsoft.copilot.eclipse.ui.CopilotUi; import com.microsoft.copilot.eclipse.ui.dialogs.SignInConfirmDialog; import com.microsoft.copilot.eclipse.ui.dialogs.SignInDialog; import com.microsoft.copilot.eclipse.ui.i18n.Messages; @@ -48,7 +50,6 @@ public Object execute(ExecutionEvent event) throws ExecutionException { } } catch (Exception e) { handleSignInException(shell, e); - // TODO log & send telemetry } return null; @@ -106,7 +107,7 @@ private void handleSignInException(Shell shell, Exception e) { String msg = Messages.signInHandler_msgDialog_signInFailed; if (StringUtils.isNotBlank(e.getMessage())) { msg += " " + e.getMessage(); - // TODO log & send telemetry + CopilotUi.LOGGER.log(LogLevel.ERROR, msg, e); } MessageDialog.openError(shell, Messages.signInHandler_msgDialog_signInFailedFailure, msg); } diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/SignOutHandler.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/SignOutHandler.java index 3c1c0971..54f0b5b4 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/SignOutHandler.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/SignOutHandler.java @@ -9,7 +9,9 @@ import com.microsoft.copilot.eclipse.core.CopilotCore; import com.microsoft.copilot.eclipse.core.CopilotStatusManager; +import com.microsoft.copilot.eclipse.core.logger.LogLevel; import com.microsoft.copilot.eclipse.core.lsp.protocol.CopilotStatusResult; +import com.microsoft.copilot.eclipse.ui.CopilotUi; import com.microsoft.copilot.eclipse.ui.i18n.Messages; import com.microsoft.copilot.eclipse.ui.utils.SwtUtils; @@ -38,7 +40,6 @@ public Object execute(ExecutionEvent event) throws ExecutionException { } } catch (Exception e) { handleSignOutException(shell, e); - // TODO: log & send telemetry } return null; @@ -48,7 +49,7 @@ private void handleSignOutException(Shell shell, Exception e) { String msg = Messages.signOutHandler_msgDialog_signOutFailed; if (StringUtils.isNotBlank(e.getMessage())) { msg += " " + e.getMessage(); - // TODO: log & send telemetry + CopilotUi.LOGGER.log(LogLevel.ERROR, e); } MessageDialog.openError(shell, Messages.signOutHandler_msgDialog_signOutFailedFailure, msg); } diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/utils/UiUtils.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/utils/UiUtils.java index 4df81a79..6fc1e0ed 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/utils/UiUtils.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/utils/UiUtils.java @@ -18,7 +18,9 @@ import org.eclipse.ui.browser.IWorkbenchBrowserSupport; import org.eclipse.ui.texteditor.ITextEditor; +import com.microsoft.copilot.eclipse.core.logger.LogLevel; import com.microsoft.copilot.eclipse.core.utils.PlatformUtils; +import com.microsoft.copilot.eclipse.ui.CopilotUi; /** * Utilities for Eclipse UI. @@ -53,7 +55,7 @@ public static boolean openLink(String link) { IWebBrowser browser = browserSupport.createBrowser(IWorkbenchBrowserSupport.AS_EXTERNAL, null, null, null); browser.openURL(new URI(encodedUrl).toURL()); } catch (Exception e) { - // TODO: log & send telemetry + CopilotUi.LOGGER.log(LogLevel.ERROR, e); return false; } return true; @@ -91,7 +93,7 @@ public static int getCaretOffset(ITextViewer textViewer) { public static int modelOffset2WidgetOffset(ITextViewer textViewer, int offset) { return textViewer instanceof ITextViewerExtension5 extension ? extension.modelOffset2WidgetOffset(offset) : offset; } - + /** * Builds an image descriptor from a PNG file at the given path. */ From 1e266a09fd83893fcd5947c1594859134c0bb257 Mon Sep 17 00:00:00 2001 From: Sheng Chen Date: Tue, 24 Dec 2024 09:43:09 +0800 Subject: [PATCH 032/690] fix - NPE when close a read-only editor (#51) * When editor is readonly, members of CompletionHandler may be null. Should take this into consideration when disposing it. --- .../ui/completion/CompletionHandler.java | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionHandler.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionHandler.java index 8717d56d..165ec81e 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionHandler.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionHandler.java @@ -181,10 +181,13 @@ private String getCategory() { * Disposes the resources of this completion handler. */ public void dispose() { - if (this.completionManager != null) { - this.completionManager.dispose(); - this.completionManager = null; + if (this.completionManager == null) { + // null manager means the handler is not initialized. + return; } + + this.completionManager.dispose(); + this.completionManager = null; lsConnection.disconnectDocument(this.documentUri); try { this.document.removePositionCategory(this.getCategory()); @@ -192,14 +195,12 @@ public void dispose() { CopilotUi.LOGGER.log(LogLevel.ERROR, e); } this.document.removePositionUpdater(this.positionUpdater); - if (this.textViewer != null) { - SwtUtils.invokeOnDisplayThread(() -> { - if (this.textViewer.getTextWidget() != null) { - this.textViewer.getTextWidget().removeCaretListener(this); - } - this.textViewer.removeTextListener(this); - }); - } + SwtUtils.invokeOnDisplayThread(() -> { + if (this.textViewer.getTextWidget() != null) { + this.textViewer.getTextWidget().removeCaretListener(this); + } + this.textViewer.removeTextListener(this); + }); } From 2ec00bedfa095cff67d0992adfe13b0275d74c24 Mon Sep 17 00:00:00 2001 From: Sheng Chen Date: Tue, 24 Dec 2024 11:02:34 +0800 Subject: [PATCH 033/690] fix - Update the cursor position from event (#49) * fix - Update the cursor position from event and rename variable --- .../core/completion/CompletionProvider.java | 2 -- .../eclipse/ui/completion/CompletionHandler.java | 4 ++-- .../copilot/eclipse/ui/utils/UiUtils.java | 16 +++++++--------- 3 files changed, 9 insertions(+), 13 deletions(-) diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/completion/CompletionProvider.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/completion/CompletionProvider.java index 38082366..0743cdb7 100644 --- a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/completion/CompletionProvider.java +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/completion/CompletionProvider.java @@ -9,9 +9,7 @@ import org.eclipse.core.runtime.jobs.IJobChangeListener; import org.eclipse.lsp4j.Position; -import com.microsoft.copilot.eclipse.core.CopilotCore; import com.microsoft.copilot.eclipse.core.CopilotStatusManager; -import com.microsoft.copilot.eclipse.core.logger.LogLevel; import com.microsoft.copilot.eclipse.core.lsp.CopilotLanguageServerConnection; import com.microsoft.copilot.eclipse.core.lsp.protocol.CompletionDocument; import com.microsoft.copilot.eclipse.core.lsp.protocol.CompletionParams; diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionHandler.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionHandler.java index 165ec81e..7fdb2974 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionHandler.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionHandler.java @@ -139,8 +139,8 @@ public CompletionCollection getCompletions() { @Override public void caretMoved(CaretEvent event) { - int caretOffset = UiUtils.getCaretOffset(this.textViewer); - this.triggerPosition = new org.eclipse.jface.text.Position(caretOffset); + int modelOffset = UiUtils.widgetOffset2ModelOffset(textViewer, event.caretOffset); + this.triggerPosition = new org.eclipse.jface.text.Position(modelOffset); // it's guaranteed that the document change event comes earlier than caret // change event. See org.eclipse.swt.custom.StyledText#modifyContent() diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/utils/UiUtils.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/utils/UiUtils.java index 6fc1e0ed..37b7d18d 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/utils/UiUtils.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/utils/UiUtils.java @@ -77,21 +77,19 @@ public static ImageDescriptor resizeIcon(String path, int width, int height) { } /** - * Gets the caret offset of the given text viewer. + * Returns the widget offset that corresponds to the given offset in the viewer's input document or -1 if + * there is no such offset. */ - public static int getCaretOffset(ITextViewer textViewer) { - if (textViewer == null) { - return 0; - } - return textViewer.getSelectedRange().x; + public static int modelOffset2WidgetOffset(ITextViewer textViewer, int offset) { + return textViewer instanceof ITextViewerExtension5 extension ? extension.modelOffset2WidgetOffset(offset) : offset; } /** - * Returns the widget offset that corresponds to the given offset in the viewer's input document or -1 if + * Returns the offset of the viewer's input document that corresponds to the given widget offset or -1 if * there is no such offset. */ - public static int modelOffset2WidgetOffset(ITextViewer textViewer, int offset) { - return textViewer instanceof ITextViewerExtension5 extension ? extension.modelOffset2WidgetOffset(offset) : offset; + public static int widgetOffset2ModelOffset(ITextViewer textViewer, int offset) { + return textViewer instanceof ITextViewerExtension5 extension ? extension.widgetOffset2ModelOffset(offset) : offset; } /** From a7fa37158a80aaac1a260a8ca0bf014d2902885a Mon Sep 17 00:00:00 2001 From: Sheng Chen Date: Tue, 24 Dec 2024 11:03:12 +0800 Subject: [PATCH 034/690] fix - Remove unused listener (#52) --- .../eclipse/ui/completion/CompletionHandler.java | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionHandler.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionHandler.java index 7fdb2974..b5962e63 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionHandler.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionHandler.java @@ -7,10 +7,8 @@ import org.eclipse.jface.text.BadPositionCategoryException; import org.eclipse.jface.text.DefaultPositionUpdater; import org.eclipse.jface.text.IDocument; -import org.eclipse.jface.text.ITextListener; import org.eclipse.jface.text.ITextOperationTarget; import org.eclipse.jface.text.ITextViewer; -import org.eclipse.jface.text.TextEvent; import org.eclipse.jface.text.TextSelection; import org.eclipse.lsp4e.LSPEclipseUtils; import org.eclipse.swt.custom.CaretEvent; @@ -29,7 +27,7 @@ * A class to listen events which are completion related and notify the completion manager to render the ghost text or * apply the suggestion to document. */ -public class CompletionHandler implements ITextListener, CaretListener { +public class CompletionHandler implements CaretListener { private CopilotLanguageServerConnection lsConnection; private CompletionProvider provider; @@ -114,7 +112,6 @@ public void acceptFullSuggestion() { void registerListeners() { SwtUtils.invokeOnDisplayThread(() -> { this.textViewer.getTextWidget().addCaretListener(this); - this.textViewer.addTextListener(this); }); } @@ -162,14 +159,6 @@ public void caretMoved(CaretEvent event) { } - @Override - public void textChanged(TextEvent event) { - // this event comes earlier than caret change event. So we should check if the typed characters - // are the same as the ghost. Then determine whether a re-redering or a new completion - // request is needed. - // TODO: check changed text - } - /** * Get category for the position updater of this document. */ @@ -199,7 +188,6 @@ public void dispose() { if (this.textViewer.getTextWidget() != null) { this.textViewer.getTextWidget().removeCaretListener(this); } - this.textViewer.removeTextListener(this); }); } From 4c8dccc2466e3aeb8481bbf7352e49215b013ed0 Mon Sep 17 00:00:00 2001 From: Sheng Chen Date: Tue, 24 Dec 2024 16:21:53 +0800 Subject: [PATCH 035/690] fix - Typo fix (#62) --- .../copilot/eclipse/core/lsp/LsStreamConnectionProvider.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/LsStreamConnectionProvider.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/LsStreamConnectionProvider.java index 92ce2738..50728ac2 100644 --- a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/LsStreamConnectionProvider.java +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/LsStreamConnectionProvider.java @@ -30,7 +30,7 @@ public class LsStreamConnectionProvider extends ProcessStreamConnectionProvider public static final String EDITOR_NAME = "Eclipse"; - public static final String EDITOR_PLUGIN_NAME = "GotHub Copilot for Eclipse"; + public static final String EDITOR_PLUGIN_NAME = "GitHub Copilot for Eclipse"; @Override public Object getInitializationOptions(@Nullable URI rootUri) { From 17db215303934559f61aa829107cb6f2632c137e Mon Sep 17 00:00:00 2001 From: ethanyhou <149548697+ethanyhou@users.noreply.github.com> Date: Tue, 24 Dec 2024 16:52:35 +0800 Subject: [PATCH 036/690] feat - Enabled completion loading spinner while copilot is completing document. (#44) --- .../core/CopilotStatusManagerTests.java | 10 +- .../eclipse/core/CopilotStatusManager.java | 10 +- .../core/completion/CompletionProvider.java | 26 +++- .../completion/CompletionStatusListener.java | 18 +++ .../icons/spinner/1.png | Bin 0 -> 481 bytes .../icons/spinner/1@2x.png | Bin 0 -> 1603 bytes .../icons/spinner/2.png | Bin 0 -> 466 bytes .../icons/spinner/2@2x.png | Bin 0 -> 1670 bytes .../icons/spinner/3.png | Bin 0 -> 464 bytes .../icons/spinner/3@2x.png | Bin 0 -> 1684 bytes .../icons/spinner/4.png | Bin 0 -> 462 bytes .../icons/spinner/4@2x.png | Bin 0 -> 1640 bytes .../icons/spinner/5.png | Bin 0 -> 467 bytes .../icons/spinner/5@2x.png | Bin 0 -> 1659 bytes .../icons/spinner/6.png | Bin 0 -> 465 bytes .../icons/spinner/6@2x.png | Bin 0 -> 1763 bytes .../icons/spinner/7.png | Bin 0 -> 465 bytes .../icons/spinner/7@2x.png | Bin 0 -> 1724 bytes .../icons/spinner/8.png | Bin 0 -> 465 bytes .../icons/spinner/8@2x.png | Bin 0 -> 1686 bytes .../plugin.properties | 3 +- com.microsoft.copilot.eclipse.ui/plugin.xml | 8 + .../copilot/eclipse/ui/CopilotUi.java | 23 +-- .../copilot/eclipse/ui/UiConstants.java | 5 + .../completion/CompletionStatusListener.java | 16 -- .../completion/CompletionStatusManager.java | 34 +++++ .../eclipse/ui/handlers/CopilotHandler.java | 5 + .../ui/handlers/ShowStatusBarMenuHandler.java | 141 ++++++++++++++++-- .../ui/handlers/ViewFeedbackForumHandler.java | 28 ++++ .../copilot/eclipse/ui/i18n/Messages.java | 16 +- .../eclipse/ui/i18n/messages.properties | 16 +- .../copilot/eclipse/ui/utils/UiUtils.java | 14 +- 32 files changed, 305 insertions(+), 68 deletions(-) create mode 100644 com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/completion/CompletionStatusListener.java create mode 100644 com.microsoft.copilot.eclipse.ui/icons/spinner/1.png create mode 100644 com.microsoft.copilot.eclipse.ui/icons/spinner/1@2x.png create mode 100644 com.microsoft.copilot.eclipse.ui/icons/spinner/2.png create mode 100644 com.microsoft.copilot.eclipse.ui/icons/spinner/2@2x.png create mode 100644 com.microsoft.copilot.eclipse.ui/icons/spinner/3.png create mode 100644 com.microsoft.copilot.eclipse.ui/icons/spinner/3@2x.png create mode 100644 com.microsoft.copilot.eclipse.ui/icons/spinner/4.png create mode 100644 com.microsoft.copilot.eclipse.ui/icons/spinner/4@2x.png create mode 100644 com.microsoft.copilot.eclipse.ui/icons/spinner/5.png create mode 100644 com.microsoft.copilot.eclipse.ui/icons/spinner/5@2x.png create mode 100644 com.microsoft.copilot.eclipse.ui/icons/spinner/6.png create mode 100644 com.microsoft.copilot.eclipse.ui/icons/spinner/6@2x.png create mode 100644 com.microsoft.copilot.eclipse.ui/icons/spinner/7.png create mode 100644 com.microsoft.copilot.eclipse.ui/icons/spinner/7@2x.png create mode 100644 com.microsoft.copilot.eclipse.ui/icons/spinner/8.png create mode 100644 com.microsoft.copilot.eclipse.ui/icons/spinner/8@2x.png delete mode 100644 com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionStatusListener.java create mode 100644 com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionStatusManager.java create mode 100644 com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/ViewFeedbackForumHandler.java diff --git a/com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/CopilotStatusManagerTests.java b/com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/CopilotStatusManagerTests.java index 2286368d..dabf9401 100644 --- a/com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/CopilotStatusManagerTests.java +++ b/com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/CopilotStatusManagerTests.java @@ -39,24 +39,18 @@ void testCopilotStatusResultOnSuccess() { } @Test - void testCheckStatusLoadingWithDelay() throws InterruptedException { + void testCheckStatusOK() throws InterruptedException { String mockedUser = "mockedUser"; // Arrange CopilotStatusResult expectedResult = new CopilotStatusResult(); expectedResult.setStatus(CopilotStatusResult.OK); expectedResult.setUser(mockedUser); CompletableFuture future = new CompletableFuture<>(); - when(mockConnection.checkStatus(false)).thenReturn(future); + future.complete(expectedResult); - // Act copilotStatusManager.checkStatus(); - // Assert initial status is LOADING - assertEquals(CopilotStatusResult.LOADING, copilotStatusManager.getCopilotStatus()); - - future.complete(expectedResult); - // Assert final status is OK assertEquals(CopilotStatusResult.OK, copilotStatusManager.getCopilotStatus()); } diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/CopilotStatusManager.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/CopilotStatusManager.java index d1936eb7..ca0e6049 100644 --- a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/CopilotStatusManager.java +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/CopilotStatusManager.java @@ -28,7 +28,7 @@ public class CopilotStatusManager { public CopilotStatusManager(CopilotLanguageServerConnection connection) { this.connection = connection; this.copilotStatusResult = new CopilotStatusResult(); - this.copilotStatusResult.setStatus(CopilotStatusResult.LOADING); + this.copilotStatusResult.setStatus(CopilotStatusResult.OK); } /** @@ -73,6 +73,14 @@ public CopilotStatusResult signOut() throws InterruptedException, ExecutionExcep } return result; } + + /** + * Set the status to OK. + */ + public CopilotStatusResult setCompletionDone() { + this.copilotStatusResult.setStatus(CopilotStatusResult.OK); + return this.copilotStatusResult; + } /** * Check the login status for current machine. diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/completion/CompletionProvider.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/completion/CompletionProvider.java index 0743cdb7..9da84e08 100644 --- a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/completion/CompletionProvider.java +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/completion/CompletionProvider.java @@ -23,6 +23,7 @@ public class CompletionProvider implements IJobChangeListener { private CompletionJob completionJob; private Set completionListeners; + private Set completionStatusListeners; private CopilotStatusManager statusManager; /** @@ -33,6 +34,7 @@ public CompletionProvider(CopilotLanguageServerConnection lsConnection, CopilotS this.completionJob = new CompletionJob(lsConnection); this.completionJob.addJobChangeListener(this); this.completionListeners = new LinkedHashSet<>(); + this.completionStatusListeners = new LinkedHashSet<>(); } /** @@ -66,6 +68,13 @@ public void triggerCompletion(String uriString, Position position, int documentV public void addCompletionListener(CompletionListener listener) { this.completionListeners.add(listener); } + + /** + * Register a completion status listener. + */ + public void addCompletionStatusListener(CompletionStatusListener listener) { + this.completionStatusListeners.add(listener); + } /** * Remove a completion listener. @@ -73,11 +82,20 @@ public void addCompletionListener(CompletionListener listener) { public void removeCompletionListener(CompletionListener listener) { this.completionListeners.remove(listener); } + + /** + * Unregister a completion status listener. + */ + public void removeCompletionStatusListener(CompletionStatusListener listener) { + listener.onCompletionDone(); + this.completionStatusListeners.remove(listener); + } @Override public void aboutToRun(IJobChangeEvent event) { - // do nothing - + for (CompletionStatusListener listener : this.completionStatusListeners) { + listener.onCompletionAboutToRun(); + } } @Override @@ -88,6 +106,10 @@ public void awake(IJobChangeEvent event) { @Override public void done(IJobChangeEvent event) { + for (CompletionStatusListener listener : this.completionStatusListeners) { + listener.onCompletionDone(); + } + IStatus jobStatus = this.completionJob.getResult(); if (jobStatus != null && !jobStatus.isOK()) { return; diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/completion/CompletionStatusListener.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/completion/CompletionStatusListener.java new file mode 100644 index 00000000..267b5c1b --- /dev/null +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/completion/CompletionStatusListener.java @@ -0,0 +1,18 @@ +package com.microsoft.copilot.eclipse.core.completion; + +/** + * Listener for completion status to track the completion progress. + */ +public interface CompletionStatusListener { + + /** + * Notifies to the listeners when the completion is about to run. + */ + void onCompletionAboutToRun(); + + /** + * Notifies to the listeners when the completion is done. + */ + void onCompletionDone(); + +} diff --git a/com.microsoft.copilot.eclipse.ui/icons/spinner/1.png b/com.microsoft.copilot.eclipse.ui/icons/spinner/1.png new file mode 100644 index 0000000000000000000000000000000000000000..2147f798fbde3cac160ba00f6ad41272e4726ee7 GIT binary patch literal 481 zcmV<70UrK|P)#a3TjHd4N;h%Rf*o8OijyLg zf+&gz#cZ`xJg?r!GfkVjB;=g;&iyVor6frxua3?-7R{?s{57QhQ@?(Gu-D4-DKxEg zHQL$Ur1i=Y%`GlO(`==*10>dv7;vIfu9h#v;pdkDbsu}Qv07P%Zm`}(NALeT(A7Xn z3`E&6C(XDk@zyCBLa;*^4Uc*F@W3 z^O&tsbb)9aev|!ec@MV0<}#a)S=?sW*PvUhx1c{{_rR6d+#GgG(yhWhK%#*}n-fE9 zCKc_04>p(Cd@`!GNECg{HUby*LG{y_M8?rsheeq+loWCMN#Sn3&kaMBj(FIRD*~kn2MxLBE^W-d!n}YGja{iCU=n>We@lOCb8z!FKL}3qH0FcEM zQjVNLF-@-cn)bfrH~7xzqXR2O!~{NCr}vZHjU0XPKLUP3sDr^r@b~28-5rDR3E9Aj zf7f~G%~^AuqpO;H@eL;b$)^_=rJqCrX@m(--%ZFp0rVE88^=>qe{K#Zugi$H`MA8l zi+-P(XX3;>i=S!`Mu5&O<}HZa#FI-3IWJu495|O|dXwoIH%(V|lITnZt{ONZJC2=g z$&hysjmzauDi?Q9NtwgZe1-Th-RT{?sA&zJ(bT%AM(c>aF)z6#K#CM{PB@1thQ-qe zOV4P!)Irn5?UC&|k5E|KP48B@sB8#~$U1IBF0oTtW1~}QtN1Dth^<3%2;enb^Z3Yw z2Ep{4(Udu;CbucRW}fH;8w#-S#DKpu0^Ea_(m;~VYa_MzUh5*ca?SbCq0rVE%@QTBW_ zM5FGQee@vh>y^Fd=qs}_0NbFkQca=lJ_QNnKY+RXV)9eAU-&xhg6J!p;;;E_;&%)7 z3SKBg5&8hx+Vk<}8f*WUlKwrdG=HIm__L8C>AG&3)^^f#i9`Hx(S`*gu=;DgR8tA$ zr4DiL;_TKZ*s8jsLr~qGCA(%9U4PI^HtSv(s9dOgkC7J%IvkF5ys&zuOidRY zd$bWv%|{M^hg4&B{!pl-WA6<`TatrG3b`jeCLs&Hb0F$Q8xomVuWw~s?wlKAHz zQ-8f2PFzn71HkE0`)BNTS+^(dI|v8{gAd8F{CGHVgF!TKF$V4qa}J#Ou)#-C-NP63 zNRF_D&ZnUIQzdrNvwQ*KUOlAyP;X*ahTJ_k&XhYSQ);K$oO)kmOHDVqf)VTeBPsCq z6kIB)eKzs3#+rO#*<0Wpd7{=$4ApMRG+o<`icB)k|Gvsro4jK`2fU3~jmrBeAg~#!j zS8>s&7I}2&Q8*nE!srtuxPI-6B{%yc6?mUy(BKow7FaaG01}&dde}GknRKWpAyv7l zRBm6m2s0`zO!Na|_aHCG9-fG1Cu72*u2l4NVspsh=jL)hGtPh^&3dW_+i3yRwy}$?O+ejb6I5Pq?B) zU)c%Kw~3a87Lj0>u6_v`5~Bu@sEdaLO zL)ra6Ug8v+7uQVq#0sojY^7%VgKz}XjpN(5Z?6s~Z-fx=T@ccC(azaT4au!>QT2V> z@kLoH?5_dR&4$jso*{P+jfX0oGz4Px5B%Fu2Q5~eN9?ceq9H>d8EW8@A!yG-`|RXB z)qE}YNOtZn8x5(fG54D-;-;mn^$$)5W)yC3mhS)n002ovPDHLkV1m;? B5CH%H literal 0 HcmV?d00001 diff --git a/com.microsoft.copilot.eclipse.ui/icons/spinner/2.png b/com.microsoft.copilot.eclipse.ui/icons/spinner/2.png new file mode 100644 index 0000000000000000000000000000000000000000..ac886be1fa6a5cff819fda43f0c718f16ab85d3a GIT binary patch literal 466 zcmV;@0WJQCP){b;%#_&Ce2*}IVU;id)%jGSq5cu(HkB?(+Ja_8H``Y?)BN}O_@)Jsxgi5 z_;4Tgb~a&cV?C_0HKarf69Z1{wAwAWe;UF2WD1ML>8z00|6|U%{mZ+8{_h7=`{VN~AQ2NgDl0WYJ!IWozSqvL9uAPrW411` zJ;I8?v)DqvV1J?`nwwhuI8GC;rOIRC3xeC39k2<@*(20N5oa+b*ko*#H0l07*qo IM6N<$g0bPtyZ`_I literal 0 HcmV?d00001 diff --git a/com.microsoft.copilot.eclipse.ui/icons/spinner/2@2x.png b/com.microsoft.copilot.eclipse.ui/icons/spinner/2@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..60b04c0449775bb9eedc0408e5f57f9ac252aff8 GIT binary patch literal 1670 zcmV;126_33P)ga|%% z=FTjS!D6A<7E}AfQ0)4nKl;=3U(>E>p%PFDCQbiM&ski>RFIcWa>AWEm-~J9+;hHj zHY#cXPf26r3LV2)3=BDMVEHy2J6cv!bLUV9t%z+8WmO4R zalXubZY})J5STp8t`$4EHBSXMFnseAFgasjxe9}uZ^U;bHFYY!TTAmHdqYoA1iZHC z+t7Xo38h#_3wlORb2lG=4#6JY2VM)mM?Ck$C*SS=sL0I+@qVYz>sV5cBuYHo?jk3@ zOI^$Vno~_eT2EI~hQJRsNWkFn8*yE_!@n)fA@->NSYZ)DiDuFrM{%3NdsOj9 zb8Cp~I^AD4GB&Yucyuy8bbCTs*Y+KU1avH4HY0^1lxL4t?szq;PsF%*oLy`eX# z1CT8NJ9C~JRn4<%H@n}qfxli*TVXVC&wGy_eH80|Wm)Zw_C7T;JZQS(AJ zpeU8l{+&8nh%KSX)-rGVYg3{F;s9!OR?l!!xVV7fxP&1s6vM4r3UH9`JtZtr>KK0# zreS29?d6uEwpSw{s9TPvxA!G9o3Go0bH(6^#S+kgwjiK^7PkPI7jdro)TOOBSHy^) zMxo+Cex5>L{5U!~v8gdIC>6LkDX*5)FP4D8&BfqpBj6(=@^aW;Spb|~v3MezGU>;n}YTcaVZ0bu# za5RgrPfD#MlG4JP7hP37+{eY>?s9KnV+7pQIG{|m6Q$A?h>6N`3L8KmNH&~U%Scjz zTarMCB+zwERYl%eEP-@K>oT>IOUSM!N;RBX!*+$iRaP;1H}n$WMHxT>CfO7_&nqnC zLLw!)e7`Yek8R zd{BINo}R=$084Vjpc0nPJNj9ofWF`$idtCYAkF8Nw=`FYph!Z0*slU`bL4%_eWP1U zjeuerK76N1-s~N()4#3IJp~rY+psHHLWR8nu`-vgxd>0q^7$0Uz4EA)^CuyRBKK6^ z@Wdyxl1or z>);*GlI z&g(uj%ZVPJ76_Dj_{~^EpuH;8p;x9ET$LJ$WmwzS?zaQbk?wD|}qSQW{pnB%N4Yjnm9_mL0 zR7(gxiPF##1umwbetZ^dJG=V1{6fyI;@(fIPx#07*qoM6N<$f|aTs+yDRo literal 0 HcmV?d00001 diff --git a/com.microsoft.copilot.eclipse.ui/icons/spinner/3.png b/com.microsoft.copilot.eclipse.ui/icons/spinner/3.png new file mode 100644 index 0000000000000000000000000000000000000000..0d8986b9d74be44af3d1064add81bb5a0c45a89e GIT binary patch literal 464 zcmV;>0WbcEP)JXe1p%kQ| zh)~Q{JH>PHMP6Q$<}QJEZr(ZH&5er?=)R)+a|x1Unse;^G4Evh$;E9>pSyr#zrBCDEW)v?vvO?czSP^r|^$>a_iBdhWxd{1L;_-kEQbcb9X%ch0%z zoF@=WV@M30bBpRYgM#9^6*T99U#rB@86)^VMj9y3tww$MwaBkvxPL)&0}`5j>pv$T z)N-aQBl7E7d_GAHeoy+fAUi?`6w};NsfEpwTDSoG=hAGpC&MnKxr2m+l;PqYN?>gb zUbnsmdFfrpH{QJY`{JuN#y$xAdT04|JXlHylvtgGWM7=Y6O*Jv_DmypGN&317Sy^r zh;p))Tg{pBYfzxR?XdaA?XhJyNAIl&-2OAsw&&P3-U?P!^iTrnRov1w)yP}f+~3>Y zJM0a?=;-$n%YYkVALYU<6DrECMghJ3(B(b*{FmT-fIwQ$1!NQ#D6w+FNd=_qaa)D~ zdDXUlFa7y(ueSN1H_wds7S!XmNP{?|3SV4Zg|8VMhXakB1A!{Xp+Fv3rJxoCA`+0S zbL|ppu~!5{2pPEdyQg}MQ?>Y_P>%y3Bam5z0%B8y%0nZ*0wpAAQDL$UrA;@N#8+Zj zqR!PS)FRE~(J%4MDXMj5K#=H)Db7%A;e@sc5j@jDYKO$aGH?^l$!T-}0-7nad7kDM zGc{~ft_hh7YLHz{vtbY->ul_^Gp-$G&kH%5(FO2-5Uy$%gh+86`?3K1Bmw`}t6C14 z2`d6Q!%7GVCF5K(^Wx=lKnw*nE&W$W#Vi9t;~ACX^$KD&?5hbH*Vc4BvTwBZ+GT)o|Fcah zYwt@lCxG67+VLCIj$nR$&wUx)J06G(caM~OeiI>)tizkKOk88mX0CBV9WIqv-O;mk z4Of)o_fxd46RVBrZtiAux3Crs7F*fHlO9so`kt3tobRZt9n-c#wrTfM#SQ2c68Td) zsIYKrq3~!zP||GfRF#)Pb(B+VdI^I>1ab|#v>SfEc{|t=MqAq z%;I^tjCE%ABWNZxb*)EZZ{7MU`p%udV}2POeXVKtmnnpR)XFUiB`PU2t4ao?fyFXZ zO!soq9h0>jXjTf$NThG}y*oNO`rOsAv8e0U@6Fq>_tXO39gS$gBz2ry1kHrTu+3M# zXa70%&YmF+f!=;VLo6fP()lJ7t|!)3qa3ImYUmfkAUQ)))Ba4ZzH45#5#H4HMOxQ> zzi{W?Ga>@Bf=7C z#N>{KkC7;11)c~qUb@rtC;?=MDw=(w#_j=Ld10+9y|50aM%YZaR6;${KyuP#0vT~N zE(z!MN>{en=vHrcpHZLw{>rKe-E<*zzp}6l1DBYpLn0A?kjXZ=g;UGn8g?;hMpE=> z*~Iz83X~_VcB|Wa&hq%?+snyhzQl@h6f{y!+#6Tsi6L^*be{RM_Qe!ej#HDhNL}B4 zB22>R8(ss|vvev~b=m{cb`<3H5^#}P*|=X_@yUU3AU-m{W^0g-lN2q=gZNOxjZi~w em#U`uW&962Wn#xBu)%Wx0000BEq~1oOIN!jwJSEZN+SmO@ zN}dOlevdlP14R$!8l>qsm)QMho^c(fu{94K`P@^{Rfrwc!t?zhz7} z5o{Y^6Ot{)UA$`Sp2`kA()U0pY@&+2au1NWjzmW$Ci-R;?LiDSA=zSb8V)D&M>uzM z_Sqhky+&+alk0LKuX8szxLWv_3T`{$WSN+h`ThhL0Ae5~N-3R0-v9sr07*qoM6N<$ Ef@9~m0N>8CZP)@IuLQ79!AXO}g3M2-# z=Y8K(3PqGLuXDPHAl5A|*|He7M7LjT;t%^Hmh9|#4=sZwauMc9o`f9UzR%}<-tTic zad9v46mY`w15R9~bqg&T4?YIQl{tmAaj)W`&hzZ3v#1`27+#n%fDsP6#f|?vK)MMr zY8wt2j}P~h^1@0^5SD4&h?#`I4NK-|ueE-zzxWl2bzlP6fw8-`@Yt z8hg)p@Nq4V&rM&uj}umATG94`T4D2ooF`QgU4|L^vTLxfunzke52~E{pFwQRsu2ge z0HC}7Ug-MW$MsVHa1GP;a4XH?M79lw_d9SHBGfxC=Lz6kVFJglf#cTp%f8||WYX}0 zuN;IX|KA{1DU2j!>9`^TQ0ofbX$OEH6{WnUQf!4a;r#mG{i@bW;X)@q4~GB6jw2X885j$92k(XH9va@?Od+%lIPHZ1d#X`<3Wt5QC$GuQV*uJFVJ8J( zn*4jCplbobvCoFi=GP!pQjc4xb9oOV*_&KV!p62Mvhx@KbxZ*?Jg#_g_zidq4Zq$y zJOcoJR7e3d4JdxDD})H`py6klhh_lCnKB7bIO_%(mOx#$75l91Bfi#iw?KfR8t$$Q zMC6{Yh%-#&;yw_crQ_>xoBuo6DZ}GyV0Y)WL9QKtN!2f(nueaoo`t4yP7S^ddxT~2 zO$zYdf;Ko%C+^mI(1BS+Gsjb0Pf8h{xV1bn&bmK0Dnv9=Y?w z(@y$M>Gi&$<%M79b;PAqlXA5^C-G1;yDd(*R>8S`83y@e%u4dT+k*@yn2$b!} zt?-jGa&_g=W3XGa_UES>F=sn@RR*Be>i_1TtOdA=#R9h9N@r!J8FQtc5O>%Uj0zFy8SmhV#RAa&0#W`MdPk(l8GLrw(;pS0@T=a_ z_ueEWK%|*TzS>H3`F5f!a-v*F!&OU_hKUBeDa}YwmJL11cc4emJNgeb3-8ZGzHDzT zUawq4Jy2T6R>%Nto@4GFGUG`xipiN4u>yt(c7GkwF%3kqkl)Aa`&K0z$ZZI_1VfDk zC0PQ&jm~Ru-BU%{|t7HJd`$7Qd3ma+p4M#_206<#qq5u?xR>=ZKU}v!lt6)x?G6-)9 zE#olKDN`Z*I`8ld0I2=vDFC!~?HsimvoK7sx+6nuJ04}r8ikK&S*8<>{9RzfTj0Au zrE3{f(7Ij#4GRj&rxZfRqwVyLGtA;sSRUXoV3JA46sKZ%mFTa!`XM2rmC@lZRsYg za`oI$#URdKyuX8{>4HwUh}yn9xjK-c0*LbFA(g>9G$q7}#@;ory0|yJ`CvfZA{ajDeK$NL!q~j=)Rc1wmiwvW7W zw+1#>u=T+9LURUZ;bK1$`%M#UuEge2)!0@f?MJ4{UN!1|jbnq&6>Pn#A8>5!M`FKe z^7o#8OLQuu5%|kFy>w+&1nZZM|FM$7yubFCw0sg@J0Xt002ov JPDHLkV1iB@(pdli literal 0 HcmV?d00001 diff --git a/com.microsoft.copilot.eclipse.ui/icons/spinner/5@2x.png b/com.microsoft.copilot.eclipse.ui/icons/spinner/5@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..66ede48bb22c732e25ab4b108b1c4ba96d63d63f GIT binary patch literal 1659 zcmV->288*EP)V6ee>sX4&^*G-g}e2m8=uiPKO7(NaMKp-{0<+NxB_ZD~uTrM>so&%0s6CWu+0 z*!#Zky`>aI*#JMbshc3HS^OZ&68|hqOm-80?#F&?|CX#f?`=!KMXZ3n$w_m2?|q-= zocEmPoSTy~hb0KYGa5V2&$r|H{AS5gFxxPSVyg%FW47kN+bkybnwIGk%agq7Li z#f;vE87qtQnR{g(bArC6Ca)_+tUi}%E?$5oPi#7RlOK5HLJK;wwy z761Wn_eB*vL!dr{-d72vUpDi-ubM+>Ck3TM8d&5q`Q?I-NMie0u7wFffBHUBCDo9Zxn_WH$G7RpbZ}t0s+f`GcyP@aFHrX0FFsF z>oII}N_CJpOiSkwBlKQotU>V;D=*s3JZj7~i8r;7W2KOMuFkLH9pY6L6q57=L`%-H z1d*l2r-nRj*@I`+Ru2|w_N&R4O2EFy+Mu>w{Lso{Z9@QmJXb9-_{df>i<>ePG`qGt zqPdzqQZbyNcB37?z0eApXU9dFCK7@Q`K-VpR~v_h7O6K@8;2G{=2n2&3LMx5N@vr7 zmAWIFjmGpt^H{LQV%j$}c9JM?kiu|v$jLMS8Shx zHH6}}N-wtQ{bbF&dxu@xRS3Y<_$jx&pT_6aA>}pP>A6HcSFogeGTkQO8gQ|QW zsa6)-Xcf$tK;>R^0QOIWzPykS&;4{k1&7K4@aq;9oysu=qsvV})JOp>Y-DrF#NAd7 z?=t$xYx7P*PJGK0#NR{DNLYFFIhAe@rEBzhXGqkj4^bV5o3*OfU{Jf;8}{L43sr&* z99j(?OB7T!P8_xdanb+Tg0?fF_*}Yt7HQj}uP{vNdQRh;GrF?WjSS_jsUAZ2cezf! z@W>aTDSe>zU@i0no!&1MKiGt_S)eO17)X?|R;)k{D3H`&W%r^@UIi^#SKcP;Y{D5` zbwJe6WaSTGQm^m~y>x*j{@p&r8Re^q8mprCB-an?J zGHqr_9wlK5$x9#UfFrUBR8QH?o>q><#uaGCidxn@8lWV=46}u88TH=Nbx{SUrY0#X zt)K)<{Hc|h(LAq-ctG0+HO*wSfWZRI7uEY_WLV!!l(w$(DtLx~C4_gV1VUol!)3FH zyjI}EI(x^^3<6Xa)Sk}bH?)n>B>;Q_yi7uvlc4R%Vi!)9dWcftiY})U(FO?rF5&D9 z0&4luW8jfx)({puNg++XK-Y+_6JOOef=*CD%7_v)7J&kr_6;V(_=hBX7bF`GpG{Wo z#mQtM4aHjAppG<<2_*H!kaQTJ;c8*G+fOR9drv9a%q{kAr1 zBG_E8g@Vn)SwsrE4LuNg3wo^fAe7j`0`@@CgUTMj@jyOT-`X zd+3LF210Mmdr)>8(Yz7IWk;@aCpfMzy-WqColjIUzXA*ZqQNJHlVLgu00000NkvXX Hu0mjfPHWL1 literal 0 HcmV?d00001 diff --git a/com.microsoft.copilot.eclipse.ui/icons/spinner/6@2x.png b/com.microsoft.copilot.eclipse.ui/icons/spinner/6@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..5f38df02e988bc35452e0791675f5c2e5166e729 GIT binary patch literal 1763 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 literal 0 HcmV?d00001 diff --git a/com.microsoft.copilot.eclipse.ui/plugin.properties b/com.microsoft.copilot.eclipse.ui/plugin.properties index 4aadecf3..0b65d477 100644 --- a/com.microsoft.copilot.eclipse.ui/plugin.properties +++ b/com.microsoft.copilot.eclipse.ui/plugin.properties @@ -3,4 +3,5 @@ command.acceptFullSuggestion.name=Accept Suggestion command.discardSuggestion.name=Discard Suggestion command.copilotForEclipsePlugin.name=GitHub Copilot for Eclipse command.signInToGitHub.name=Sign in to GitHub Copilot -command.signOutFromGitHub.name=Sign out from GitHub Copilot \ No newline at end of file +command.signOutFromGitHub.name=Sign out from GitHub Copilot +command.viewFeedbackForum.name=View Feedback Forum \ No newline at end of file diff --git a/com.microsoft.copilot.eclipse.ui/plugin.xml b/com.microsoft.copilot.eclipse.ui/plugin.xml index 466def52..f2786167 100644 --- a/com.microsoft.copilot.eclipse.ui/plugin.xml +++ b/com.microsoft.copilot.eclipse.ui/plugin.xml @@ -51,6 +51,10 @@ id="com.microsoft.copilot.eclipse.commands.discardSuggestion" name="%command.discardSuggestion.name"> + +
@@ -80,6 +84,10 @@ class="com.microsoft.copilot.eclipse.ui.handlers.DiscardSuggestionHandler" commandId="com.microsoft.copilot.eclipse.commands.discardSuggestion"> + + diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/CopilotUi.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/CopilotUi.java index b5dc6d23..8fffbdd6 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/CopilotUi.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/CopilotUi.java @@ -7,12 +7,10 @@ import org.osgi.framework.BundleContext; import com.microsoft.copilot.eclipse.core.CopilotCore; -import com.microsoft.copilot.eclipse.core.completion.CompletionListener; -import com.microsoft.copilot.eclipse.core.completion.CompletionProvider; import com.microsoft.copilot.eclipse.core.logger.CopilotForEclipseLogger; import com.microsoft.copilot.eclipse.core.logger.LogLevel; import com.microsoft.copilot.eclipse.core.lsp.CopilotLanguageServerConnection; -import com.microsoft.copilot.eclipse.ui.completion.CompletionStatusListener; +import com.microsoft.copilot.eclipse.ui.completion.CompletionStatusManager; import com.microsoft.copilot.eclipse.ui.completion.EditorLifecycleListener; import com.microsoft.copilot.eclipse.ui.completion.EditorsManager; import com.microsoft.copilot.eclipse.ui.utils.SwtUtils; @@ -25,7 +23,7 @@ public class CopilotUi extends Plugin { private static final int RETRY_COUNT = 30; private static CopilotUi COPILOT_UI_PLUGIN = null; - private CompletionStatusListener completionStatusListener; + private CompletionStatusManager completionStatusManager; private EditorLifecycleListener editorLifecycleListener; private EditorsManager editorsManager; public static final CopilotForEclipseLogger LOGGER = new CopilotForEclipseLogger(CopilotCore.class.getName()); @@ -63,19 +61,24 @@ public void start(BundleContext context) throws Exception { this.editorsManager = new EditorsManager(connection, CopilotCore.getPlugin().getCompletionProvider()); this.editorLifecycleListener = new EditorLifecycleListener(editorsManager); - this.completionStatusListener = new CompletionStatusListener(); + this.completionStatusManager = new CompletionStatusManager(); registerPartListener(); - registerCompletionListener(); + addCompletionStatusListener(); // Initialize the completion handler for the active editor in case we miss the event // to initialize it. initCompletionHandlerForActiveEditor(); } + + public CompletionStatusManager getCompletionStatusManager() { + return completionStatusManager; + } @Override public void stop(BundleContext context) throws Exception { unregisterPartListener(); + removeCompletionStatusListener(); if (this.editorsManager != null) { this.editorsManager.dispose(); } @@ -92,8 +95,8 @@ private void registerPartListener() { } } - private void registerCompletionListener() { - CopilotCore.getPlugin().getCompletionProvider().addCompletionListener(this.completionStatusListener); + private void addCompletionStatusListener() { + CopilotCore.getPlugin().getCompletionProvider().addCompletionStatusListener(this.completionStatusManager); } private void initCompletionHandlerForActiveEditor() { @@ -110,8 +113,8 @@ private void unregisterPartListener() { } } - private void unregisterCompletionListener() { - CopilotCore.getPlugin().getCompletionProvider().removeCompletionListener(this.completionStatusListener); + private void removeCompletionStatusListener() { + CopilotCore.getPlugin().getCompletionProvider().removeCompletionStatusListener(this.completionStatusManager); } } diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/UiConstants.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/UiConstants.java index f6df7406..da347bad 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/UiConstants.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/UiConstants.java @@ -17,4 +17,9 @@ private UiConstants() { */ public static final int DEFAULT_GHOST_TEXT_SCALE = 112; + + /** + * The URL constants for the Copilot menu. + */ + public static final String COPILOT_FEEDBACK_FORUM_URL = "https://github.com/orgs/community/discussions/categories/copilot"; } diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionStatusListener.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionStatusListener.java deleted file mode 100644 index 01a6e637..00000000 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionStatusListener.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.microsoft.copilot.eclipse.ui.completion; - -import com.microsoft.copilot.eclipse.core.completion.CompletionCollection; -import com.microsoft.copilot.eclipse.core.completion.CompletionListener; -import com.microsoft.copilot.eclipse.ui.utils.UiUtils; - -/** - * Listener for tracking copilot completion status. - */ -public class CompletionStatusListener implements CompletionListener { - - @Override - public void onCompletionResolved(CompletionCollection completions) { - // do nothing - } -} \ No newline at end of file diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionStatusManager.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionStatusManager.java new file mode 100644 index 00000000..30ef37c1 --- /dev/null +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionStatusManager.java @@ -0,0 +1,34 @@ +package com.microsoft.copilot.eclipse.ui.completion; + +import com.microsoft.copilot.eclipse.core.completion.CompletionStatusListener; +import com.microsoft.copilot.eclipse.ui.utils.UiUtils; + +/** + * Listener for tracking copilot completion status. + */ +public class CompletionStatusManager implements CompletionStatusListener { + + private boolean completionInProgress; + + /** + * Constructor for the CompletionStatusManager. + */ + public CompletionStatusManager() { + } + + @Override + public void onCompletionAboutToRun() { + this.completionInProgress = true; + UiUtils.refreshCopilotMenu(); + } + + @Override + public void onCompletionDone() { + this.completionInProgress = false; + UiUtils.refreshCopilotMenu(); + } + + public boolean isCompletionInProgress() { + return completionInProgress; + } +} \ No newline at end of file diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/CopilotHandler.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/CopilotHandler.java index 61070cc2..b3692fea 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/CopilotHandler.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/CopilotHandler.java @@ -7,6 +7,7 @@ import com.microsoft.copilot.eclipse.core.lsp.CopilotLanguageServerConnection; import com.microsoft.copilot.eclipse.ui.CopilotUi; import com.microsoft.copilot.eclipse.ui.completion.CompletionHandler; +import com.microsoft.copilot.eclipse.ui.completion.CompletionStatusManager; import com.microsoft.copilot.eclipse.ui.completion.EditorsManager; /** @@ -32,4 +33,8 @@ public CompletionHandler getActiveCompletionHandler() { public CopilotLanguageServerConnection getLanguageServerConnection() { return CopilotCore.getPlugin().getCopilotLanguageServer(); } + + public CompletionStatusManager getCompletionStatusManager() { + return CopilotUi.getPlugin().getCompletionStatusManager(); + } } diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/ShowStatusBarMenuHandler.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/ShowStatusBarMenuHandler.java index 7753b768..5ce7a299 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/ShowStatusBarMenuHandler.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/ShowStatusBarMenuHandler.java @@ -1,10 +1,14 @@ package com.microsoft.copilot.eclipse.ui.handlers; +import java.util.Map; import java.util.Objects; -import org.eclipse.core.commands.AbstractHandler; import org.eclipse.core.commands.ExecutionEvent; import org.eclipse.core.commands.ExecutionException; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Status; +import org.eclipse.core.runtime.jobs.Job; import org.eclipse.jface.action.Action; import org.eclipse.jface.action.MenuManager; import org.eclipse.jface.action.Separator; @@ -12,24 +16,27 @@ import org.eclipse.swt.widgets.Menu; import org.eclipse.swt.widgets.Shell; import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.commands.IElementUpdater; import org.eclipse.ui.handlers.HandlerUtil; import org.eclipse.ui.handlers.IHandlerService; +import org.eclipse.ui.menus.UIElement; import com.microsoft.copilot.eclipse.core.CopilotCore; import com.microsoft.copilot.eclipse.core.CopilotStatusManager; import com.microsoft.copilot.eclipse.core.logger.LogLevel; import com.microsoft.copilot.eclipse.core.lsp.protocol.CopilotStatusResult; import com.microsoft.copilot.eclipse.ui.CopilotUi; +import com.microsoft.copilot.eclipse.ui.completion.CompletionStatusManager; import com.microsoft.copilot.eclipse.ui.i18n.Messages; import com.microsoft.copilot.eclipse.ui.utils.UiUtils; /** * Handler for showing GitHub Copilot status bar menu. */ -public class ShowStatusBarMenuHandler extends AbstractHandler { - +public class ShowStatusBarMenuHandler extends CopilotHandler implements IElementUpdater { private IHandlerService handlerService; private CopilotStatusManager copilotStatusManager; + private SpinnerJob spinnerJob; @Override public Object execute(ExecutionEvent event) throws ExecutionException { @@ -37,10 +44,17 @@ public Object execute(ExecutionEvent event) throws ExecutionException { copilotStatusManager = CopilotCore.getPlugin().getCopilotStatusManager(); MenuManager menuManager = new MenuManager(); + // Sign in status section addStatusAction(menuManager); + // References to the usage of GitHub Copilot section + // TODO: Uncomment to enable the feedback forum link + // menuManager.add(new Separator()); + // addLinkToFeedbackForumAction(menuManager); + + // Sign in & sign out section + menuManager.add(new Separator()); if (!Objects.equals(copilotStatusManager.getCopilotStatus(), CopilotStatusResult.LOADING)) { - menuManager.add(new Separator()); addSignInOrSignOutAction(menuManager); } @@ -50,32 +64,88 @@ public Object execute(ExecutionEvent event) throws ExecutionException { return null; } + @Override + public void updateElement(UIElement element, Map parameters) { + CompletionStatusManager completionStatusManager = getCompletionStatusManager(); + + if (completionStatusManager.isCompletionInProgress()) { + scheduleSpinnerJob(element); + } else { + // Since spinner job has 200ms delay, cancel the spinner job if it is running to avoid flickering. + if (spinnerJob != null) { + spinnerJob.cancel(); + } + + String copilotStatus = CopilotCore.getPlugin().getCopilotStatusManager().getCopilotStatus(); + String iconPath = null; + + switch (copilotStatus) { + case CopilotStatusResult.OK: + iconPath = "/icons/github_copilot_signed_in_blue.png"; + break; + case CopilotStatusResult.LOADING: + scheduleSpinnerJob(element); + break; + case CopilotStatusResult.ERROR: + case CopilotStatusResult.WARNING: + iconPath = "/icons/github_copilot_error_blue.png"; + break; + case CopilotStatusResult.NOT_SIGNED_IN: + case CopilotStatusResult.NOT_AUTHORIZED: + default: + iconPath = "/icons/github_copilot_not_signed_in_blue.png"; + } + + if (iconPath != null) { + ImageDescriptor newIcon = UiUtils.buildImageDescriptorFromPngPath(iconPath); + element.setIcon(newIcon); + } + } + } + private void addStatusAction(MenuManager menuManager) { - String signInStatus = getSignInStatusBasedOnAuthResult(copilotStatusManager.getCopilotStatus()); - String signInStatusTitle = Messages.menu_signInStatus + ": " + signInStatus; + String copilotStatus = getCopilotStatusBasedOnAuthAndCompletionResult(copilotStatusManager.getCopilotStatus()); + String copilotStatusTitle = Messages.menu_copilotStatus + ": " + copilotStatus; - MenuActionFactory.createMenuAction(menuManager, signInStatusTitle, handlerService, signInStatus, false); + MenuActionFactory.createMenuAction(menuManager, copilotStatusTitle, handlerService, copilotStatus, false); } - private String getSignInStatusBasedOnAuthResult(String copilotStatus) { + private void addLinkToFeedbackForumAction(MenuManager menuManager) { + MenuActionFactory.createMenuAction(menuManager, Messages.menu_viewFeedbackForum, handlerService, + "com.microsoft.copilot.eclipse.commands.viewFeedbackForum", true); + } + + private String getCopilotStatusBasedOnAuthAndCompletionResult(String copilotStatus) { + CompletionStatusManager completionStatusManager = getCompletionStatusManager(); switch (copilotStatus) { case CopilotStatusResult.OK: - return Messages.menu_signInStatus_ready; + return completionStatusManager.isCompletionInProgress() ? Messages.menu_copilotStatus_completionInProgress + : Messages.menu_copilotStatus_ready; case CopilotStatusResult.ERROR: - return Messages.menu_signInStatus_unknownError; + return Messages.menu_copilotStatus_unknownError; case CopilotStatusResult.LOADING: - return Messages.menu_signInStatus_loading; + return Messages.menu_copilotStatus_loading; case CopilotStatusResult.NOT_SIGNED_IN: - return Messages.menu_signInStatus_notSignedInToGitHub; + return Messages.menu_copilotStatus_notSignedInToGitHub; case CopilotStatusResult.WARNING: - return Messages.menu_signInStatus_agentWarning; + return Messages.menu_copilotStatus_agentWarning; case CopilotStatusResult.NOT_AUTHORIZED: - return Messages.menu_signInStatus_notAuthorized; + return Messages.menu_copilotStatus_notAuthorized; default: - return Messages.menu_signInStatus_loading; + return Messages.menu_copilotStatus_loading; } } + private void scheduleSpinnerJob(UIElement uiElement) { + if (spinnerJob != null) { + spinnerJob.cancel(); + } else { + spinnerJob = new SpinnerJob(); + } + spinnerJob.setTargetUiElement(uiElement); + spinnerJob.schedule(); + } + private void addSignInOrSignOutAction(MenuManager menuManager) { if (Objects.equals(copilotStatusManager.getCopilotStatus(), CopilotStatusResult.OK)) { ImageDescriptor signInIcon = UiUtils.buildImageDescriptorFromPngPath("/icons/signin.png"); @@ -110,4 +180,45 @@ public static void createMenuAction(MenuManager menuManager, String text, IHandl createMenuAction(menuManager, text, null, handlerService, commandId, enabled); } } + + private class SpinnerJob extends Job { + private static final int INITIAL_ICON_INDEX = 1; + private static final int TOTAL_SPINNER_ICONS = 8; + private static final long COMPLETION_IN_PROGRESS_SPINNER_ROTATE_RATE_MILLIS = 200L; + + + private int currentIconIndex = INITIAL_ICON_INDEX; + private UIElement uiElement; + + public SpinnerJob() { + super("Spinner Job"); + this.setSystem(true); + } + + public void setTargetUiElement(UIElement uiElement) { + this.uiElement = uiElement; + } + + @Override + protected IStatus run(IProgressMonitor monitor) { + try { + if (this.uiElement == null) { + throw new IllegalStateException("UI element is not set. Spinner cannot be set."); + } + String iconPath = String.format("/icons/spinner/%d.png", currentIconIndex); + ImageDescriptor newIcon = UiUtils.buildImageDescriptorFromPngPath(iconPath); + this.uiElement.setIcon(newIcon); + currentIconIndex = (currentIconIndex % TOTAL_SPINNER_ICONS) + 1; + if (CopilotUi.getPlugin().getCompletionStatusManager().isCompletionInProgress()) { + schedule(COMPLETION_IN_PROGRESS_SPINNER_ROTATE_RATE_MILLIS); + } else { + cancel(); + } + } catch (Exception e) { + CopilotCore.LOGGER.log(LogLevel.ERROR, e); + return Status.CANCEL_STATUS; + } + return Status.OK_STATUS; + } + } } diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/ViewFeedbackForumHandler.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/ViewFeedbackForumHandler.java new file mode 100644 index 00000000..9831fc82 --- /dev/null +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/ViewFeedbackForumHandler.java @@ -0,0 +1,28 @@ +package com.microsoft.copilot.eclipse.ui.handlers; + +import org.eclipse.core.commands.AbstractHandler; +import org.eclipse.core.commands.ExecutionEvent; +import org.eclipse.core.commands.ExecutionException; + +import com.microsoft.copilot.eclipse.core.logger.LogLevel; +import com.microsoft.copilot.eclipse.ui.CopilotUi; +import com.microsoft.copilot.eclipse.ui.UiConstants; +import com.microsoft.copilot.eclipse.ui.utils.UiUtils; + +/** + * Open the Copilot feedback forum in browser. + */ +public class ViewFeedbackForumHandler extends AbstractHandler { + + @Override + public Object execute(ExecutionEvent event) throws ExecutionException { + try { + UiUtils.openLink(UiConstants.COPILOT_FEEDBACK_FORUM_URL); + } catch (Exception e) { + CopilotUi.LOGGER.log(LogLevel.ERROR, e); + } + + return null; + } + +} diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/i18n/Messages.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/i18n/Messages.java index fcf7264b..02f42a04 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/i18n/Messages.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/i18n/Messages.java @@ -7,15 +7,17 @@ */ public final class Messages extends NLS { private static final String BUNDLE_NAME = "com.microsoft.copilot.eclipse.ui.i18n.messages"; //$NON-NLS-1$ - public static String menu_signInStatus; - public static String menu_signInStatus_ready; - public static String menu_signInStatus_loading; - public static String menu_signInStatus_notSignedInToGitHub; - public static String menu_signInStatus_unknownError; - public static String menu_signInStatus_notAuthorized; - public static String menu_signInStatus_agentWarning; + public static String menu_copilotStatus; + public static String menu_copilotStatus_ready; + public static String menu_copilotStatus_loading; + public static String menu_copilotStatus_completionInProgress; + public static String menu_copilotStatus_notSignedInToGitHub; + public static String menu_copilotStatus_unknownError; + public static String menu_copilotStatus_notAuthorized; + public static String menu_copilotStatus_agentWarning; public static String menu_signToGitHub; public static String menu_signOutFromGitHub; + public static String menu_viewFeedbackForum; public static String signInDialog_title; public static String signInDialog_button_cancel; public static String signInDialog_button_copyOpen; diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/i18n/messages.properties b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/i18n/messages.properties index 5a1d1015..52e9adb9 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/i18n/messages.properties +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/i18n/messages.properties @@ -1,12 +1,14 @@ -menu_signInStatus=Status -menu_signInStatus_ready=Ready -menu_signInStatus_loading=Loading -menu_signInStatus_notSignedInToGitHub=Not signed in to GitHub -menu_signInStatus_unknownError=Unknown error -menu_signInStatus_notAuthorized=No access to GitHub Copilot -menu_signInStatus_agentWarning=Copilot is encountering temporary issues +menu_copilotStatus=Status +menu_copilotStatus_ready=Ready +menu_copilotStatus_loading=Loading +menu_copilotStatus_completionInProgress=Copilot completing in progress +menu_copilotStatus_notSignedInToGitHub=Not signed in to GitHub +menu_copilotStatus_unknownError=Unknown error +menu_copilotStatus_notAuthorized=No access to GitHub Copilot +menu_copilotStatus_agentWarning=Copilot is encountering temporary issues menu_signToGitHub=Sign In to GitHub menu_signOutFromGitHub=Sign Out from GitHub +menu_viewFeedbackForum=View Feedback Forum... signInDialog_title=Sign In to GitHub signInDialog_button_cancel=Cancel diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/utils/UiUtils.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/utils/UiUtils.java index 37b7d18d..87448fa7 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/utils/UiUtils.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/utils/UiUtils.java @@ -16,6 +16,8 @@ import org.eclipse.ui.PlatformUI; import org.eclipse.ui.browser.IWebBrowser; import org.eclipse.ui.browser.IWorkbenchBrowserSupport; +import org.eclipse.ui.commands.ICommandService; +import org.eclipse.ui.menus.UIElement; import org.eclipse.ui.texteditor.ITextEditor; import com.microsoft.copilot.eclipse.core.logger.LogLevel; @@ -95,7 +97,17 @@ public static int widgetOffset2ModelOffset(ITextViewer textViewer, int offset) { /** * Builds an image descriptor from a PNG file at the given path. */ - public static final ImageDescriptor buildImageDescriptorFromPngPath(String path) { + public static ImageDescriptor buildImageDescriptorFromPngPath(String path) { return ImageDescriptor.createFromURL(UiUtils.class.getResource(path)); } + + /** + * Refreshes the elements of the command with the given ID. + */ + public static void refreshCopilotMenu() { + ICommandService commandService = PlatformUI.getWorkbench().getService(ICommandService.class); + if (commandService != null) { + commandService.refreshElements("com.microsoft.copilot.eclipse.commands.showStatusBarMenu", null); + } + } } From 9f78a31d980c03a3fb36c06f5c626047d8dc83e8 Mon Sep 17 00:00:00 2001 From: Sheng Chen Date: Tue, 24 Dec 2024 16:59:27 +0800 Subject: [PATCH 037/690] fix - take the completion range into consideration when accept suggestion (#55) --- .../core/completion/CompletionCollection.java | 22 ++++ .../META-INF/MANIFEST.MF | 4 +- .../ui/completion/CompletionManagerTests.java | 112 ++++++++++++++++++ .../ui/completion/CompletionManager.java | 10 +- 4 files changed, 142 insertions(+), 6 deletions(-) create mode 100644 com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/completion/CompletionManagerTests.java diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/completion/CompletionCollection.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/completion/CompletionCollection.java index bc1ad2f6..bb5a87c4 100644 --- a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/completion/CompletionCollection.java +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/completion/CompletionCollection.java @@ -3,6 +3,8 @@ import java.util.List; import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.lsp4j.Position; +import org.eclipse.lsp4j.Range; import com.microsoft.copilot.eclipse.core.CopilotCore; import com.microsoft.copilot.eclipse.core.logger.LogLevel; @@ -96,6 +98,26 @@ public int getNumberOfLines() { return this.getText().split("\n").length; } + /** + * Get the position where the completion was triggered. + */ + public Position getTriggerPosition() { + if (this.completions.isEmpty()) { + throw new IllegalStateException("completions cannot be empty"); + } + return this.completions.get(index).getPosition(); + } + + /** + * Get the range for the completion. + */ + public Range getRange() { + if (this.completions.isEmpty()) { + throw new IllegalStateException("completions cannot be empty"); + } + return this.completions.get(index).getRange(); + } + public List getUuids() { return this.completions.stream().map(CompletionItem::getUuid).toList(); } diff --git a/com.microsoft.copilot.eclipse.ui.test/META-INF/MANIFEST.MF b/com.microsoft.copilot.eclipse.ui.test/META-INF/MANIFEST.MF index b17ef99d..a31cae6f 100644 --- a/com.microsoft.copilot.eclipse.ui.test/META-INF/MANIFEST.MF +++ b/com.microsoft.copilot.eclipse.ui.test/META-INF/MANIFEST.MF @@ -19,4 +19,6 @@ Require-Bundle: org.mockito.mockito-core;bundle-version="5.14.2", org.eclipse.ui.ide, org.eclipse.ui.workbench.texteditor, org.eclipse.jface.text;bundle-version="3.25.200", - org.eclipse.lsp4j;bundle-version="0.23.1" + org.eclipse.lsp4j;bundle-version="0.23.1", + org.eclipse.core.resources, + org.eclipse.lsp4j.jsonrpc;bundle-version="0.23.1" diff --git a/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/completion/CompletionManagerTests.java b/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/completion/CompletionManagerTests.java new file mode 100644 index 00000000..e40314ee --- /dev/null +++ b/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/completion/CompletionManagerTests.java @@ -0,0 +1,112 @@ +package com.microsoft.copilot.eclipse.ui.completion; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.jface.text.IDocument; +import org.eclipse.jface.text.ITextOperationTarget; +import org.eclipse.jface.text.ITextViewer; +import org.eclipse.lsp4j.Position; +import org.eclipse.lsp4j.Range; +import org.eclipse.ui.IEditorPart; +import org.eclipse.ui.IWorkbench; +import org.eclipse.ui.IWorkbenchWindow; +import org.eclipse.ui.PartInitException; +import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.texteditor.ITextEditor; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import com.microsoft.copilot.eclipse.core.completion.CompletionCollection; +import com.microsoft.copilot.eclipse.core.completion.CompletionProvider; +import com.microsoft.copilot.eclipse.core.lsp.CopilotLanguageServerConnection; +import com.microsoft.copilot.eclipse.core.lsp.protocol.CompletionItem; +import com.microsoft.copilot.eclipse.ui.utils.SwtUtils; + +@ExtendWith(MockitoExtension.class) +class CompletionManagerTests { + + private IProject project; + + @Mock + private CopilotLanguageServerConnection mockLsConnection; + + @BeforeEach + public void setUp() throws Exception { + project = ResourcesPlugin.getWorkspace().getRoot().getProject("TestProject"); + project.create(null); + project.open(null); + } + + @AfterEach + public void tearDown() throws Exception { + project.delete(true, null); + } + + @Test + void testReplaceCompletion_1() throws Exception { + IFile file = project.getFile("Test.java"); + String content = """ + public class App { + + public void hi() { + System.out.println(""); + } + + } + """; + file.create(content.getBytes(), IResource.FORCE, null); + int documentVersion = 1; + + IEditorPart editorPart = getEditorPartFor(file); + assertTrue(editorPart instanceof ITextEditor); + + ITextEditor textEditor = (ITextEditor) editorPart; + ITextViewer textViewer = (ITextViewer) textEditor.getAdapter(ITextOperationTarget.class); + IDocument document = textEditor.getDocumentProvider().getDocument(textEditor.getEditorInput()); + + when(mockLsConnection.getDocumentVersion(any())).thenReturn(documentVersion); + CompletionManager manager = new CompletionManager(mockLsConnection, mock(CompletionProvider.class), textViewer, + document, file.getLocationURI()); + + List completions = List.of(new CompletionItem("uuid", " System.out.println(\"hi\");", + new Range(new Position(3, 0), new Position(3, 27)), "hi\");", new Position(3, 24), documentVersion)); + CompletionCollection completionsCollection = new CompletionCollection(completions, + file.getLocationURI().toASCIIString()); + + manager.onCompletionResolved(completionsCollection); + manager.acceptSuggestion(); + + assertTrue(document.get().contains(" System.out.println(\"hi\");\n")); + } + + protected IEditorPart getEditorPartFor(IFile file) { + AtomicReference ref = new AtomicReference<>(); + SwtUtils.invokeOnDisplayThread(() -> { + IWorkbench workbench = PlatformUI.getWorkbench(); + IWorkbenchWindow window = workbench.getActiveWorkbenchWindow(); + if (window != null) { + try { + ref.set(window.getActivePage().openEditor(new org.eclipse.ui.part.FileEditorInput(file), + "org.eclipse.ui.DefaultTextEditor")); + } catch (PartInitException e) { + } + } + }); + return ref.get(); + } + +} diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionManager.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionManager.java index a2bf110b..41f5ec6b 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionManager.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionManager.java @@ -57,7 +57,7 @@ public CompletionManager(CopilotLanguageServerConnection lsConnection, Completio this.documentUri = documentUri; this.completions = null; - this.triggerPosition = new org.eclipse.jface.text.Position(0); + this.triggerPosition = new Position(0); this.textViewer = textViewer; StyledText styledText = textViewer.getTextWidget(); if (styledText != null) { @@ -179,16 +179,16 @@ public boolean hasCompletion() { * @throws BadLocationException if the offset is invalid. */ public void acceptSuggestion() throws BadLocationException { - int offset = this.triggerPosition.offset; - if (offset < 0) { + if (this.completions == null || this.completions.getSize() == 0) { return; } - + int startOffset = LSPEclipseUtils.toOffset(this.completions.getTriggerPosition(), this.document); String text = this.completions.getText(); if (StringUtils.isEmpty(text)) { return; } - this.document.replace(offset, 0, text); + int endOffset = LSPEclipseUtils.toOffset(this.completions.getRange().getEnd(), this.document); + this.document.replace(startOffset, endOffset - startOffset, text); } public CompletionCollection getCompletions() { From f06e13d4d775a1d7aacbf0c40cc20539d2c7a6b7 Mon Sep 17 00:00:00 2001 From: Sheng Chen Date: Tue, 24 Dec 2024 17:00:23 +0800 Subject: [PATCH 038/690] fix - Add timeout for completion job (#48) --- .../core}/completion/CompletionJobTests.java | 34 +++++++++++++++++-- .../core/completion/CompletionJob.java | 13 ++++++- 2 files changed, 44 insertions(+), 3 deletions(-) rename {com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui => com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core}/completion/CompletionJobTests.java (65%) diff --git a/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/completion/CompletionJobTests.java b/com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/completion/CompletionJobTests.java similarity index 65% rename from com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/completion/CompletionJobTests.java rename to com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/completion/CompletionJobTests.java index d1fa4362..5c5cb4ee 100644 --- a/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/completion/CompletionJobTests.java +++ b/com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/completion/CompletionJobTests.java @@ -1,19 +1,25 @@ -package com.microsoft.copilot.eclipse.ui.completion; +package com.microsoft.copilot.eclipse.core.completion; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import java.util.ArrayList; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.NullProgressMonitor; +import org.eclipse.core.runtime.Status; +import org.eclipse.core.runtime.jobs.IJobManager; +import org.eclipse.core.runtime.jobs.Job; +import org.eclipse.lsp4j.Position; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.junit.jupiter.MockitoExtension; -import com.microsoft.copilot.eclipse.core.completion.CompletionJob; import com.microsoft.copilot.eclipse.core.lsp.CopilotLanguageServerConnection; import com.microsoft.copilot.eclipse.core.lsp.protocol.CompletionDocument; import com.microsoft.copilot.eclipse.core.lsp.protocol.CompletionParams; @@ -70,4 +76,28 @@ void testTriggerCompletionJobWithParams() throws InterruptedException { assertEquals(IStatus.OK, status.getSeverity()); assertEquals(expectedResult, completionJob.getCompletionResult()); } + + @Test + void testShouldTimeoutWhenCompletionTakesTooLong() throws Exception { + when(mockLsConnection.getCompletions(any())).thenAnswer(invocation -> { + TimeUnit.SECONDS.sleep(6); // completion will timeout after 5 seconds + return new CompletableFuture<>(); + }); + + CompletionJob job = new CompletionJob(mockLsConnection); + Position position = new Position(0, 0); + CompletionDocument completionDoc = new CompletionDocument("file://test.java", position); + completionDoc.setVersion(1); + completionDoc.setInsertSpaces(true); + completionDoc.setTabSize(4); + job.setCompletionParams(new CompletionParams(completionDoc)); + job.schedule(); + + IJobManager jobManager = Job.getJobManager(); + jobManager.join(CompletionJob.COMPLETION_JOB_FAMILY, new NullProgressMonitor()); + + assertEquals(Status.CANCEL_STATUS, job.getResult()); + + } + } diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/completion/CompletionJob.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/completion/CompletionJob.java index c5933268..32f9fbf0 100644 --- a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/completion/CompletionJob.java +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/completion/CompletionJob.java @@ -2,6 +2,8 @@ import java.util.Objects; import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; @@ -20,8 +22,13 @@ */ public class CompletionJob extends Job { + /** + * The job family for completion jobs, can be used to find out this completion job. + */ public static final String COMPLETION_JOB_FAMILY = "com.microsoft.copilot.eclipse.completionJobFamily"; + private static final int COMPLETION_TIMEOUT = 5000; + private CompletionResult result; private CopilotLanguageServerConnection lsConnection; @@ -54,12 +61,16 @@ protected IStatus run(IProgressMonitor monitor) { return Status.CANCEL_STATUS; } try { - this.result = this.lsConnection.getCompletions(params).get(); + this.result = this.lsConnection.getCompletions(params).get(COMPLETION_TIMEOUT, TimeUnit.MILLISECONDS); } catch (InterruptedException e) { return Status.CANCEL_STATUS; } catch (ExecutionException e) { CopilotCore.LOGGER.log(LogLevel.ERROR, e); return new Status(IStatus.ERROR, Constants.PLUGIN_ID, e.getMessage(), e); + } catch (TimeoutException e) { + CopilotCore.LOGGER.log(LogLevel.WARNING, + "Completion request timed out after " + COMPLETION_TIMEOUT + " milliseconds"); + return Status.CANCEL_STATUS; } if (monitor.isCanceled()) { return Status.CANCEL_STATUS; From 69ff97d4ec666c9379622aab605bfc6884f9f125 Mon Sep 17 00:00:00 2001 From: Sheng Chen Date: Tue, 24 Dec 2024 18:40:43 +0800 Subject: [PATCH 039/690] fix - Reset the line vertical indentation correctly (#63) --- .../core/completion/CompletionJob.java | 6 +++--- .../ui/completion/CompletionHandler.java | 2 +- .../ui/completion/CompletionManager.java | 21 ++++++++++++++++--- 3 files changed, 22 insertions(+), 7 deletions(-) diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/completion/CompletionJob.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/completion/CompletionJob.java index 32f9fbf0..995b0e4f 100644 --- a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/completion/CompletionJob.java +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/completion/CompletionJob.java @@ -27,7 +27,7 @@ public class CompletionJob extends Job { */ public static final String COMPLETION_JOB_FAMILY = "com.microsoft.copilot.eclipse.completionJobFamily"; - private static final int COMPLETION_TIMEOUT = 5000; + private static final int COMPLETION_TIMEOUT_MILLIS = 5000; private CompletionResult result; @@ -61,7 +61,7 @@ protected IStatus run(IProgressMonitor monitor) { return Status.CANCEL_STATUS; } try { - this.result = this.lsConnection.getCompletions(params).get(COMPLETION_TIMEOUT, TimeUnit.MILLISECONDS); + this.result = this.lsConnection.getCompletions(params).get(COMPLETION_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS); } catch (InterruptedException e) { return Status.CANCEL_STATUS; } catch (ExecutionException e) { @@ -69,7 +69,7 @@ protected IStatus run(IProgressMonitor monitor) { return new Status(IStatus.ERROR, Constants.PLUGIN_ID, e.getMessage(), e); } catch (TimeoutException e) { CopilotCore.LOGGER.log(LogLevel.WARNING, - "Completion request timed out after " + COMPLETION_TIMEOUT + " milliseconds"); + "Completion request timed out after " + COMPLETION_TIMEOUT_MILLIS + " milliseconds"); return Status.CANCEL_STATUS; } if (monitor.isCanceled()) { diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionHandler.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionHandler.java index b5962e63..75749bef 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionHandler.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionHandler.java @@ -127,7 +127,7 @@ public void triggerCompletion() { * Clear the completion ghost text. */ public void clearCompletionRendering() { - this.completionManager.clearGhostText(this.triggerPosition); + this.completionManager.clearGhostText(); } public CompletionCollection getCompletions() { diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionManager.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionManager.java index 41f5ec6b..5c76d34d 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionManager.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionManager.java @@ -85,11 +85,26 @@ public void triggerCompletion(Position position, int documentVersion) { /** * Clear the completion. */ - public void clearGhostText(Position position) { - this.triggerPosition = position; + public void clearGhostText() { + if (this.completions == null || this.completions.getSize() == 0) { + return; + } + try { + // use completion trigger position if available. this.triggerPosition is the current + // cursor position, which may not be the same as the completion trigger position when user + // use mouse to move the cursor. In that case, the line vertical indentation might not be + // reset correctly. + int offset = LSPEclipseUtils.toOffset(this.completions.getTriggerPosition(), this.document); + this.triggerPosition = new Position(offset); + } catch (BadLocationException e) { + CopilotUi.LOGGER.log(LogLevel.ERROR, e); + return; + } this.completions = null; StyledText styledText = textViewer.getTextWidget(); if (styledText != null) { + this.setLineVerticalIndentation(styledText, null, + UiUtils.modelOffset2WidgetOffset(textViewer, this.triggerPosition.getOffset())); SwtUtils.invokeOnDisplayThread(styledText::redraw, styledText); } @@ -153,7 +168,7 @@ public void paintControl(PaintEvent e) { private void setLineVerticalIndentation(StyledText styledText, GC gc, int widgetOffset) { int height = 0; - if (this.completions != null) { + if (this.completions != null && gc != null) { // Change the height (line vertical indentation) to fit the line of // ghost text. Point ghostTextExtent = gc.textExtent(this.completions.getText()); From 6a967437af0e11e0b22a4e18fa35beb558fd51e0 Mon Sep 17 00:00:00 2001 From: Sheng Chen Date: Wed, 25 Dec 2024 13:51:05 +0800 Subject: [PATCH 040/690] build - Add code sign (#67) --- .azure-pipelines/nightly.yml | 59 ++++++++++++++++++++++++++++-------- 1 file changed, 46 insertions(+), 13 deletions(-) diff --git a/.azure-pipelines/nightly.yml b/.azure-pipelines/nightly.yml index 10e2f133..bbd65cd1 100644 --- a/.azure-pipelines/nightly.yml +++ b/.azure-pipelines/nightly.yml @@ -38,6 +38,17 @@ extends: - checkout: self fetchTags: false + - task: UsePythonVersion@0 + displayName: 'Use Python 3.11.x' + inputs: + versionSpec: 3.11.x + + - task: UseDotNet@2 + displayName: 'Use .NET Core 3.1.x' + inputs: + packageType: 'sdk' + version: '3.1.x' + - task: JavaToolInstaller@0 displayName: Use Java 17 inputs: @@ -50,6 +61,15 @@ extends: inputs: version: '20.x' + - task: MicroBuildSigningPlugin@4 + displayName: 'Install Signing Plugin' + inputs: + signType: real + azureSubscription: 'MicroBuild Signing Task (MSEng)' + feedSource: 'https://mseng.pkgs.visualstudio.com/DefaultCollection/_packaging/MicroBuildToolset/nuget/v3/index.json' + env: + SYSTEM_ACCESSTOKEN: $(System.AccessToken) + - bash: npm i workingDirectory: com.microsoft.copilot.eclipse.core/copilot-agent displayName: Install Copilot LS @@ -57,24 +77,37 @@ extends: - bash: ./mvnw clean package displayName: 'Run Maven Clean and Package' - # TODO: support code sign - bash: | mkdir -p ./artifacts/eclipse/ cp ./com.microsoft.copilot.eclipse.repository/target/com.microsoft.copilot.eclipse.repository*.zip ./artifacts/eclipse/GithubCopilotForEclipse.zip - # unzip ./artifacts/eclipse/GithubCopilotForEclipse.zip "**/*.jar" "*.jar" -d ./artifacts/eclipse/folder - # rm ./artifacts/eclipse/GithubCopilotForEclipse.zip + unzip ./artifacts/eclipse/GithubCopilotForEclipse.zip "**/*.jar" "*.jar" -d ./artifacts/eclipse/folder + rm ./artifacts/eclipse/GithubCopilotForEclipse.zip + + ## Workaround: Remove MD5/SHA256 Validation in artifacts.xml + cd ./artifacts/eclipse/folder + unzip artifacts.jar -d ./artifacts + rm artifacts.jar + cd artifacts + sed -i -E '/download\.md5|checksum/d' ./artifacts.xml + zip -R ./artifacts.jar * **/* + mv ./artifacts.jar ../artifacts.jar + cd .. + rm -rf ./artifacts + displayName: Prepare plugin zip + + - task: CmdLine@2 + displayName: Sign jars + inputs: + script: | + files=$(find . -type f -name "*.jar") + for file in $files; do + dotnet "$MBSIGN_APPFOLDER/DDSignFiles.dll" -- /file:"$file" /certs:100010171 + done + workingDirectory: 'artifacts/eclipse/folder' - # ## Workaround: Remove MD5/SHA256 Validation in artifacts.xml - # cd ./artifacts/eclipse/folder - # unzip artifacts.jar -d ./artifacts - # rm artifacts.jar - # cd artifacts - # sed -i -E '/download\.md5|checksum/d' ./artifacts.xml - # zip -R ./artifacts.jar * **/* - # mv ./artifacts.jar ../artifacts.jar - # cd .. - # rm -rf ./artifacts + - bash: cd ./artifacts/eclipse/folder && zip -R ../GithubCopilotForEclipse.zip **/*.jar *.jar + displayName: Package zip - task: CopyFiles@2 displayName: Copy plugin zip From e35398da456e2f0c4c346b419e8d347569a8e03d Mon Sep 17 00:00:00 2001 From: ethanyhou <149548697+ethanyhou@users.noreply.github.com> Date: Wed, 25 Dec 2024 14:01:07 +0800 Subject: [PATCH 041/690] fix - Menu icon stuck in the error status. (#66) --- .../copilot/eclipse/core/CopilotStatusManager.java | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/CopilotStatusManager.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/CopilotStatusManager.java index ca0e6049..7f379ea8 100644 --- a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/CopilotStatusManager.java +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/CopilotStatusManager.java @@ -1,8 +1,6 @@ package com.microsoft.copilot.eclipse.core; -import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeUnit; import com.microsoft.copilot.eclipse.core.logger.LogLevel; import com.microsoft.copilot.eclipse.core.lsp.CopilotLanguageServerConnection; @@ -18,8 +16,6 @@ public class CopilotStatusManager { private CopilotStatusResult copilotStatusResult; - private static final int CHECK_STATUS_TIMEOUT_MILLIS = 3000; - /** * Constructor for the CopilotStatusManager. * @@ -86,9 +82,7 @@ public CopilotStatusResult setCompletionDone() { * Check the login status for current machine. */ public void checkStatus() { - CompletableFuture statusFuture = this.connection.checkStatus(false); - - statusFuture.orTimeout(CHECK_STATUS_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS).thenAccept(result -> { + this.connection.checkStatus(false).thenAccept(result -> { this.copilotStatusResult = result; }).exceptionally(ex -> { CopilotCore.LOGGER.log(LogLevel.ERROR, ex); From 79e250e6a5373cadcb251f9aa11485682f782673 Mon Sep 17 00:00:00 2001 From: Sheng Chen Date: Wed, 25 Dec 2024 19:03:55 +0800 Subject: [PATCH 042/690] fix - UI freeze on startup (#68) * This change wraps the UI activation in a Job to prevent the UI from freezing on startup. --- .../copilot/eclipse/core/CopilotCore.java | 14 +++- .../copilot/eclipse/ui/CopilotUiTests.java | 5 ++ .../copilot/eclipse/ui/CopilotUi.java | 66 +++++++++++-------- 3 files changed, 57 insertions(+), 28 deletions(-) diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/CopilotCore.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/CopilotCore.java index bda5ecf2..aeb37af4 100644 --- a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/CopilotCore.java +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/CopilotCore.java @@ -1,5 +1,7 @@ package com.microsoft.copilot.eclipse.core; +import java.util.Objects; + import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Plugin; @@ -28,6 +30,11 @@ public class CopilotCore extends Plugin { private static CopilotCore COPILOT_CORE_PLUGIN = null; public static final CopilotForEclipseLogger LOGGER = new CopilotForEclipseLogger(CopilotCore.class.getName()); + /** + * The job family for the initialization job. + */ + public static final String INIT_JOB_FAMILY = "com.microsoft.copilot.eclipse.core.initJob"; + /** * Creates the Copilot core plugin. The plugin is created automatically by the Eclipse framework. Clients must not * call this constructor. @@ -78,8 +85,13 @@ protected IStatus run(IProgressMonitor monitor) { initRunnable.run(); return Status.OK_STATUS; } + + @Override + public boolean belongsTo(Object family) { + return Objects.equals(INIT_JOB_FAMILY, family); + } }; - initJob.setUser(true); + initJob.setUser(false); initJob.schedule(); } diff --git a/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/CopilotUiTests.java b/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/CopilotUiTests.java index 14409d29..fc1aba10 100644 --- a/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/CopilotUiTests.java +++ b/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/CopilotUiTests.java @@ -4,9 +4,12 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import org.eclipse.core.runtime.Platform; +import org.eclipse.core.runtime.jobs.Job; import org.junit.jupiter.api.Test; import org.osgi.framework.Bundle; +import com.microsoft.copilot.eclipse.core.CopilotCore; + class CopilotUiTests { @Test @@ -14,6 +17,8 @@ void testCopilotCoreWakeUp() throws Exception { CopilotUi ui = new CopilotUi(); ui.start(null); + Job.getJobManager().join(CopilotCore.INIT_JOB_FAMILY, null); + Bundle bundle = Platform.getBundle("com.microsoft.copilot.eclipse.core"); assertNotNull(bundle); diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/CopilotUi.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/CopilotUi.java index 8fffbdd6..4db36271 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/CopilotUi.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/CopilotUi.java @@ -1,6 +1,11 @@ package com.microsoft.copilot.eclipse.ui; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.OperationCanceledException; import org.eclipse.core.runtime.Plugin; +import org.eclipse.core.runtime.Status; +import org.eclipse.core.runtime.jobs.Job; import org.eclipse.ui.IEditorPart; import org.eclipse.ui.IWorkbenchWindow; import org.eclipse.ui.PlatformUI; @@ -20,7 +25,6 @@ */ public class CopilotUi extends Plugin { - private static final int RETRY_COUNT = 30; private static CopilotUi COPILOT_UI_PLUGIN = null; private CompletionStatusManager completionStatusManager; @@ -43,34 +47,42 @@ public static CopilotUi getPlugin() { @Override public void start(BundleContext context) throws Exception { - // wake up Core plugin and wait until copilot LS is ready - // TODO: check if we can improve logic here, for example, use a listener to wait for LS ready. - CopilotLanguageServerConnection connection = null; - for (int i = 0; i < RETRY_COUNT; i++) { - connection = CopilotCore.getPlugin().getCopilotLanguageServer(); - if (connection != null) { - break; + Job initJob = new Job("Copilot initialization") { + @Override + protected IStatus run(IProgressMonitor monitor) { + try { + // wait until Core is initialized. + Job.getJobManager().join(CopilotCore.INIT_JOB_FAMILY, null); + + CopilotLanguageServerConnection connection = CopilotCore.getPlugin().getCopilotLanguageServer(); + if (connection == null) { + var ex = new IllegalStateException("Failed to start copilot language server."); + LOGGER.log(LogLevel.ERROR, ex); + throw ex; + } + + CopilotUi.this.editorsManager = new EditorsManager(connection, + CopilotCore.getPlugin().getCompletionProvider()); + CopilotUi.this.editorLifecycleListener = new EditorLifecycleListener(editorsManager); + CopilotUi.this.completionStatusManager = new CompletionStatusManager(); + + registerPartListener(); + addCompletionStatusListener(); + + // Initialize the completion handler for the active editor in case we miss the event + // to initialize it. + initCompletionHandlerForActiveEditor(); + } catch (OperationCanceledException | InterruptedException e) { + LOGGER.log(LogLevel.ERROR, e); + return Status.error("Failed to initialize GitHub Copilot plugin.", e); + } + return Status.OK_STATUS; } - Thread.sleep(1000); - } - if (connection == null) { - var ex = new IllegalStateException("Failed to start copilot language server."); - LOGGER.log(LogLevel.ERROR, ex); - throw ex; - } - - this.editorsManager = new EditorsManager(connection, CopilotCore.getPlugin().getCompletionProvider()); - this.editorLifecycleListener = new EditorLifecycleListener(editorsManager); - this.completionStatusManager = new CompletionStatusManager(); - - registerPartListener(); - addCompletionStatusListener(); - - // Initialize the completion handler for the active editor in case we miss the event - // to initialize it. - initCompletionHandlerForActiveEditor(); + }; + initJob.setSystem(true); + initJob.schedule(); } - + public CompletionStatusManager getCompletionStatusManager() { return completionStatusManager; } From c4d24bf646d7b9bf72e1cd539cd929dc49de9143 Mon Sep 17 00:00:00 2001 From: Sheng Chen Date: Thu, 26 Dec 2024 13:16:49 +0800 Subject: [PATCH 043/690] fix - UI freeze when completion happens frequently (#69) * Previously, we use 'IJobChangeListener' to listen to the job status, side effect is that when calling 'job.cancel()', it will wait the events to be notified to all listeners. Since the completion is triggered from main thread, this behavior will block the UI. As a workaround, this PR discards the use of 'IJobChangeListener'. Instead, we make the 'CompletionJob' be the inner class of 'CompletionProvider' - grammar candy that makes it be able to access the listeners registered in 'CompletionProvider' and notify the listeners directly. --- .../core/completion/CompletionJobTests.java | 26 +-- .../completion/CompletionProviderTests.java | 31 +++- .../core/completion/CompletionJob.java | 94 ----------- .../core/completion/CompletionProvider.java | 150 ++++++++++++------ 4 files changed, 133 insertions(+), 168 deletions(-) delete mode 100644 com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/completion/CompletionJob.java diff --git a/com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/completion/CompletionJobTests.java b/com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/completion/CompletionJobTests.java index 5c5cb4ee..8a7493ec 100644 --- a/com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/completion/CompletionJobTests.java +++ b/com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/completion/CompletionJobTests.java @@ -5,7 +5,6 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -import java.util.ArrayList; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; @@ -20,10 +19,10 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.junit.jupiter.MockitoExtension; +import com.microsoft.copilot.eclipse.core.completion.CompletionProvider.CompletionJob; import com.microsoft.copilot.eclipse.core.lsp.CopilotLanguageServerConnection; import com.microsoft.copilot.eclipse.core.lsp.protocol.CompletionDocument; import com.microsoft.copilot.eclipse.core.lsp.protocol.CompletionParams; -import com.microsoft.copilot.eclipse.core.lsp.protocol.CompletionResult; @ExtendWith(MockitoExtension.class) class CompletionJobTests { @@ -34,7 +33,7 @@ class CompletionJobTests { @BeforeEach public void setUp() { mockLsConnection = mock(CopilotLanguageServerConnection.class); - completionJob = new CompletionJob(mockLsConnection); + completionJob = mock(CompletionProvider.class).new CompletionJob(mockLsConnection); } @Test @@ -60,23 +59,6 @@ void testCancelCompletionJob() throws InterruptedException { } } - @Test - void testTriggerCompletionJobWithParams() throws InterruptedException { - CompletionDocument document = mock(CompletionDocument.class); - CompletionParams params = new CompletionParams(document); - completionJob.setCompletionParams(params); - - CompletionResult expectedResult = new CompletionResult(new ArrayList<>()); - CompletableFuture future = CompletableFuture.completedFuture(expectedResult); - when(mockLsConnection.getCompletions(params)).thenReturn(future); - completionJob.schedule(); - completionJob.join(); - - IStatus status = completionJob.getResult(); - assertEquals(IStatus.OK, status.getSeverity()); - assertEquals(expectedResult, completionJob.getCompletionResult()); - } - @Test void testShouldTimeoutWhenCompletionTakesTooLong() throws Exception { when(mockLsConnection.getCompletions(any())).thenAnswer(invocation -> { @@ -84,7 +66,7 @@ void testShouldTimeoutWhenCompletionTakesTooLong() throws Exception { return new CompletableFuture<>(); }); - CompletionJob job = new CompletionJob(mockLsConnection); + CompletionJob job = new CompletionProvider(mockLsConnection, null).new CompletionJob(mockLsConnection); Position position = new Position(0, 0); CompletionDocument completionDoc = new CompletionDocument("file://test.java", position); completionDoc.setVersion(1); @@ -94,7 +76,7 @@ void testShouldTimeoutWhenCompletionTakesTooLong() throws Exception { job.schedule(); IJobManager jobManager = Job.getJobManager(); - jobManager.join(CompletionJob.COMPLETION_JOB_FAMILY, new NullProgressMonitor()); + jobManager.join(CompletionProvider.COMPLETION_JOB_FAMILY, new NullProgressMonitor()); assertEquals(Status.CANCEL_STATUS, job.getResult()); diff --git a/com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/completion/CompletionProviderTests.java b/com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/completion/CompletionProviderTests.java index 49243404..48826859 100644 --- a/com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/completion/CompletionProviderTests.java +++ b/com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/completion/CompletionProviderTests.java @@ -1,5 +1,6 @@ package com.microsoft.copilot.eclipse.core.completion; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; @@ -7,9 +8,11 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import java.util.ArrayList; import java.util.List; import java.util.concurrent.CompletableFuture; +import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.NullProgressMonitor; import org.eclipse.core.runtime.OperationCanceledException; import org.eclipse.core.runtime.jobs.IJobManager; @@ -17,12 +20,15 @@ import org.eclipse.lsp4j.Position; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import com.microsoft.copilot.eclipse.core.CopilotStatusManager; import com.microsoft.copilot.eclipse.core.lsp.CopilotLanguageServerConnection; +import com.microsoft.copilot.eclipse.core.lsp.protocol.CompletionDocument; import com.microsoft.copilot.eclipse.core.lsp.protocol.CompletionItem; +import com.microsoft.copilot.eclipse.core.lsp.protocol.CompletionParams; import com.microsoft.copilot.eclipse.core.lsp.protocol.CompletionResult; import com.microsoft.copilot.eclipse.core.lsp.protocol.CopilotStatusResult; @@ -49,7 +55,7 @@ void testShouldNotifyListenersOnCompletion() throws OperationCanceledException, Position position = new Position(0, 0); completionProvider.triggerCompletion("file://test.java", position, 1); IJobManager jobManager = Job.getJobManager(); - jobManager.join(CompletionJob.COMPLETION_JOB_FAMILY, new NullProgressMonitor()); + jobManager.join(CompletionProvider.COMPLETION_JOB_FAMILY, new NullProgressMonitor()); verify(mockLsConnection, times(1)).getCompletions(any()); } @@ -63,5 +69,28 @@ void testShouldNotTriggerCompletionWhenNotSignedIn() throws OperationCanceledExc completionProvider.triggerCompletion("file://test.java", position, 1); verify(mockLsConnection, never()).getCompletions(any()); } + + @Test + void testTriggerCompletionJobWithParams() throws InterruptedException { + when(mockStatusManager.getCopilotStatus()).thenReturn(CopilotStatusResult.OK); + CompletionItem mockCompletionItem = mock(CompletionItem.class); + when(mockCompletionItem.getUuid()).thenReturn("test"); + CompletionResult expectedResult = new CompletionResult(List.of(mockCompletionItem)); + CompletableFuture future = CompletableFuture.completedFuture(expectedResult); + when(mockLsConnection.getCompletions(any())).thenReturn(future); + CompletionProvider completionProvider = new CompletionProvider(mockLsConnection, mockStatusManager); + + CompletionListener mockListener = mock(CompletionListener.class); + completionProvider.addCompletionListener(mockListener); + + completionProvider.triggerCompletion("file://test.java", new Position(0, 0), 1); + IJobManager jobManager = Job.getJobManager(); + jobManager.join(CompletionProvider.COMPLETION_JOB_FAMILY, new NullProgressMonitor()); + + ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(CompletionCollection.class); + verify(mockListener).onCompletionResolved(argumentCaptor.capture()); + + assertEquals("test", argumentCaptor.getValue().getUuids().get(0)); + } } diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/completion/CompletionJob.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/completion/CompletionJob.java deleted file mode 100644 index 995b0e4f..00000000 --- a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/completion/CompletionJob.java +++ /dev/null @@ -1,94 +0,0 @@ -package com.microsoft.copilot.eclipse.core.completion; - -import java.util.Objects; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; - -import org.eclipse.core.runtime.IProgressMonitor; -import org.eclipse.core.runtime.IStatus; -import org.eclipse.core.runtime.Status; -import org.eclipse.core.runtime.jobs.Job; - -import com.microsoft.copilot.eclipse.core.Constants; -import com.microsoft.copilot.eclipse.core.CopilotCore; -import com.microsoft.copilot.eclipse.core.logger.LogLevel; -import com.microsoft.copilot.eclipse.core.lsp.CopilotLanguageServerConnection; -import com.microsoft.copilot.eclipse.core.lsp.protocol.CompletionParams; -import com.microsoft.copilot.eclipse.core.lsp.protocol.CompletionResult; - -/** - * Job to trigger an inline completion. - */ -public class CompletionJob extends Job { - - /** - * The job family for completion jobs, can be used to find out this completion job. - */ - public static final String COMPLETION_JOB_FAMILY = "com.microsoft.copilot.eclipse.completionJobFamily"; - - private static final int COMPLETION_TIMEOUT_MILLIS = 5000; - - private CompletionResult result; - - private CopilotLanguageServerConnection lsConnection; - private CompletionParams params; - - /** - * Creates a new completion job. - */ - public CompletionJob(CopilotLanguageServerConnection lsConnection) { - super("Generating completion..."); - this.lsConnection = lsConnection; - this.setSystem(true); - this.setPriority(Job.INTERACTIVE); - } - - public void setCompletionParams(CompletionParams params) { - this.params = params; - } - - public CompletionParams getCompletionParams() { - return params; - } - - @Override - protected IStatus run(IProgressMonitor monitor) { - if (params == null) { - return new Status(IStatus.ERROR, Constants.PLUGIN_ID, "Invalid completion parameters"); - } - if (monitor.isCanceled()) { - return Status.CANCEL_STATUS; - } - try { - this.result = this.lsConnection.getCompletions(params).get(COMPLETION_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS); - } catch (InterruptedException e) { - return Status.CANCEL_STATUS; - } catch (ExecutionException e) { - CopilotCore.LOGGER.log(LogLevel.ERROR, e); - return new Status(IStatus.ERROR, Constants.PLUGIN_ID, e.getMessage(), e); - } catch (TimeoutException e) { - CopilotCore.LOGGER.log(LogLevel.WARNING, - "Completion request timed out after " + COMPLETION_TIMEOUT_MILLIS + " milliseconds"); - return Status.CANCEL_STATUS; - } - if (monitor.isCanceled()) { - return Status.CANCEL_STATUS; - } - return Status.OK_STATUS; - } - - public CompletionResult getCompletionResult() { - return result; - } - - public String getUriString() { - return params.getDoc().getUri(); - } - - @Override - public boolean belongsTo(Object family) { - return Objects.equals(family, COMPLETION_JOB_FAMILY); - } - -} diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/completion/CompletionProvider.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/completion/CompletionProvider.java index 9da84e08..cc0fa36d 100644 --- a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/completion/CompletionProvider.java +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/completion/CompletionProvider.java @@ -3,13 +3,20 @@ import java.util.LinkedHashSet; import java.util.Objects; import java.util.Set; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; -import org.eclipse.core.runtime.jobs.IJobChangeEvent; -import org.eclipse.core.runtime.jobs.IJobChangeListener; +import org.eclipse.core.runtime.Status; +import org.eclipse.core.runtime.jobs.Job; import org.eclipse.lsp4j.Position; +import com.microsoft.copilot.eclipse.core.Constants; +import com.microsoft.copilot.eclipse.core.CopilotCore; import com.microsoft.copilot.eclipse.core.CopilotStatusManager; +import com.microsoft.copilot.eclipse.core.logger.LogLevel; import com.microsoft.copilot.eclipse.core.lsp.CopilotLanguageServerConnection; import com.microsoft.copilot.eclipse.core.lsp.protocol.CompletionDocument; import com.microsoft.copilot.eclipse.core.lsp.protocol.CompletionParams; @@ -19,7 +26,12 @@ /** * Provider for inline completion. */ -public class CompletionProvider implements IJobChangeListener { +public class CompletionProvider { + + /** + * The job family for completion jobs, can be used to find out this completion job. + */ + public static final String COMPLETION_JOB_FAMILY = "com.microsoft.copilot.eclipse.completionJobFamily"; private CompletionJob completionJob; private Set completionListeners; @@ -32,7 +44,6 @@ public class CompletionProvider implements IJobChangeListener { public CompletionProvider(CopilotLanguageServerConnection lsConnection, CopilotStatusManager statusManager) { this.statusManager = statusManager; this.completionJob = new CompletionJob(lsConnection); - this.completionJob.addJobChangeListener(this); this.completionListeners = new LinkedHashSet<>(); this.completionStatusListeners = new LinkedHashSet<>(); } @@ -49,7 +60,6 @@ public void triggerCompletion(String uriString, Position position, int documentV return; } this.completionJob.cancel(); - this.completionJob.setCompletionParams(null); CompletionDocument completionDoc = new CompletionDocument(uriString, position); completionDoc.setVersion(documentVersion); // following format options are hard-coded, because eclipse support applying the format options @@ -68,7 +78,7 @@ public void triggerCompletion(String uriString, Position position, int documentV public void addCompletionListener(CompletionListener listener) { this.completionListeners.add(listener); } - + /** * Register a completion status listener. */ @@ -82,7 +92,7 @@ public void addCompletionStatusListener(CompletionStatusListener listener) { public void removeCompletionListener(CompletionListener listener) { this.completionListeners.remove(listener); } - + /** * Unregister a completion status listener. */ @@ -91,61 +101,99 @@ public void removeCompletionStatusListener(CompletionStatusListener listener) { this.completionStatusListeners.remove(listener); } - @Override - public void aboutToRun(IJobChangeEvent event) { - for (CompletionStatusListener listener : this.completionStatusListeners) { - listener.onCompletionAboutToRun(); + /** + * TODO: public for testing. + */ + public class CompletionJob extends Job { + + private static final int COMPLETION_TIMEOUT_MILLIS = 5000; + + private CopilotLanguageServerConnection lsConnection; + private CompletionParams params; + private CompletionCollection completions; + + /** + * Creates a new completion job. + */ + public CompletionJob(CopilotLanguageServerConnection lsConnection) { + super("Generating completion..."); + this.lsConnection = lsConnection; + this.setSystem(true); + this.setPriority(Job.INTERACTIVE); } - } - - @Override - public void awake(IJobChangeEvent event) { - // do nothing - - } - @Override - public void done(IJobChangeEvent event) { - for (CompletionStatusListener listener : this.completionStatusListeners) { - listener.onCompletionDone(); + public void setCompletionParams(CompletionParams params) { + this.params = params; } - IStatus jobStatus = this.completionJob.getResult(); - if (jobStatus != null && !jobStatus.isOK()) { - return; - } - CompletionResult result = this.completionJob.getCompletionResult(); - if (result == null || result.getCompletions() == null || result.getCompletions().isEmpty()) { - return; + @Override + protected IStatus run(IProgressMonitor monitor) { + notifyCompletionAboutToRun(); + this.completions = null; + try { + IStatus status = runCompletion(monitor); + if (status.isOK() && this.completions != null) { + notifyCompletionResolved(); + } + return status; + } finally { + notifyCompletionDone(); + } } - CompletionParams params = this.completionJob.getCompletionParams(); - if (params == null) { - return; + private IStatus runCompletion(IProgressMonitor monitor) { + if (params == null) { + CopilotCore.LOGGER.log(LogLevel.ERROR, "Invalid completion parameters"); + return new Status(IStatus.ERROR, Constants.PLUGIN_ID, "Invalid completion parameters"); + } + if (monitor.isCanceled()) { + return Status.CANCEL_STATUS; + } + try { + CompletionResult result = this.lsConnection.getCompletions(params).get(COMPLETION_TIMEOUT_MILLIS, + TimeUnit.MILLISECONDS); + if (result == null || result.getCompletions() == null || result.getCompletions().isEmpty()) { + return Status.OK_STATUS; + } + + this.completions = new CompletionCollection(result.getCompletions(), params.getDoc().getUri()); + } catch (InterruptedException e) { + return Status.CANCEL_STATUS; + } catch (ExecutionException e) { + CopilotCore.LOGGER.log(LogLevel.ERROR, e); + return new Status(IStatus.ERROR, Constants.PLUGIN_ID, e.getMessage(), e); + } catch (TimeoutException e) { + CopilotCore.LOGGER.log(LogLevel.WARNING, + "Completion request timed out after " + COMPLETION_TIMEOUT_MILLIS + " milliseconds"); + return Status.CANCEL_STATUS; + } + if (monitor.isCanceled()) { + return Status.CANCEL_STATUS; + } + return Status.OK_STATUS; } - CompletionCollection completions = new CompletionCollection(result.getCompletions(), params.getDoc().getUri()); - for (CompletionListener listener : this.completionListeners) { - listener.onCompletionResolved(completions); + @Override + public boolean belongsTo(Object family) { + return Objects.equals(family, COMPLETION_JOB_FAMILY); } - } - - @Override - public void running(IJobChangeEvent event) { - // do nothing - - } - @Override - public void scheduled(IJobChangeEvent event) { - // do nothing - - } + private void notifyCompletionAboutToRun() { + for (CompletionStatusListener listener : CompletionProvider.this.completionStatusListeners) { + listener.onCompletionAboutToRun(); + } + } - @Override - public void sleeping(IJobChangeEvent event) { - // do nothing + private void notifyCompletionDone() { + for (CompletionStatusListener listener : CompletionProvider.this.completionStatusListeners) { + listener.onCompletionDone(); + } + } + private void notifyCompletionResolved() { + for (CompletionListener listener : CompletionProvider.this.completionListeners) { + listener.onCompletionResolved(this.completions); + } + } } - } From 8fa2d1c2f5d117f762d89f7cae6d63fdc7475fc4 Mon Sep 17 00:00:00 2001 From: Sheng Chen Date: Thu, 26 Dec 2024 13:27:43 +0800 Subject: [PATCH 044/690] fix - Plugin localization file not work (#71) --- com.microsoft.copilot.eclipse.ui/META-INF/MANIFEST.MF | 1 + 1 file changed, 1 insertion(+) diff --git a/com.microsoft.copilot.eclipse.ui/META-INF/MANIFEST.MF b/com.microsoft.copilot.eclipse.ui/META-INF/MANIFEST.MF index be667e90..8f68ff26 100644 --- a/com.microsoft.copilot.eclipse.ui/META-INF/MANIFEST.MF +++ b/com.microsoft.copilot.eclipse.ui/META-INF/MANIFEST.MF @@ -3,6 +3,7 @@ Bundle-ManifestVersion: 2 Bundle-Name: com.microsoft.copilot.eclipse.ui Bundle-SymbolicName: com.microsoft.copilot.eclipse.ui;singleton:=true Bundle-Version: 0.1.0.qualifier +Bundle-Localization: plugin Export-Package: com.microsoft.copilot.eclipse.ui, com.microsoft.copilot.eclipse.ui.completion, com.microsoft.copilot.eclipse.ui.dialogs, From c831daf72ca094d442c5268680802e6498ff3f39 Mon Sep 17 00:00:00 2001 From: ethanyhou <149548697+ethanyhou@users.noreply.github.com> Date: Thu, 26 Dec 2024 19:54:47 +0800 Subject: [PATCH 045/690] feat - Added fallback to the lsp connection provider. (#70) --- .gitignore | 3 +- .../META-INF/MANIFEST.MF | 3 +- .../build.properties | 3 +- .../copilot-agent/package.json | 2 +- .../core/lsp/LsStreamConnectionProvider.java | 131 ++++++++++++++++-- target-platform.target | 7 + 6 files changed, 132 insertions(+), 17 deletions(-) diff --git a/.gitignore b/.gitignore index 2e352862..325b9e48 100644 --- a/.gitignore +++ b/.gitignore @@ -31,4 +31,5 @@ replay_pid* # Copilot agent **/node_modules/ -**/copilot-agent/native/ \ No newline at end of file +**/copilot-agent/native/ +**/copilot-agent/dist/ \ No newline at end of file diff --git a/com.microsoft.copilot.eclipse.core/META-INF/MANIFEST.MF b/com.microsoft.copilot.eclipse.core/META-INF/MANIFEST.MF index fccf9829..d8fcecc4 100644 --- a/com.microsoft.copilot.eclipse.core/META-INF/MANIFEST.MF +++ b/com.microsoft.copilot.eclipse.core/META-INF/MANIFEST.MF @@ -21,4 +21,5 @@ Require-Bundle: org.eclipse.lsp4e;bundle-version="0.18.12", org.apache.commons.lang3;bundle-version="3.17.0", org.eclipse.jdt.annotation;bundle-version="2.3.0", org.eclipse.jface.text, - com.google.gson;bundle-version="2.11.0" + com.google.gson;bundle-version="2.11.0", + org.eclipse.wildwebdeveloper.embedder.node;bundle-version="1.0.3";resolution:=optional diff --git a/com.microsoft.copilot.eclipse.core/build.properties b/com.microsoft.copilot.eclipse.core/build.properties index a19753f5..f892d99a 100644 --- a/com.microsoft.copilot.eclipse.core/build.properties +++ b/com.microsoft.copilot.eclipse.core/build.properties @@ -6,5 +6,6 @@ bin.includes = META-INF/,\ copilot-agent/native/darwin-arm64/,\ copilot-agent/native/darwin-x64/,\ copilot-agent/native/linux-x64/,\ - copilot-agent/native/win32-x64/ + copilot-agent/native/win32-x64/,\ + copilot-agent/dist/ diff --git a/com.microsoft.copilot.eclipse.core/copilot-agent/package.json b/com.microsoft.copilot.eclipse.core/copilot-agent/package.json index 0c16fe6a..15a58d57 100644 --- a/com.microsoft.copilot.eclipse.core/copilot-agent/package.json +++ b/com.microsoft.copilot.eclipse.core/copilot-agent/package.json @@ -9,7 +9,7 @@ "url": "https://github.com/microsoft/copilot-eclipse.git" }, "scripts": { - "postinstall": "npx --yes copyfiles -u 3 \"node_modules/@github/copilot-language-server/native/**/*\" \".\"" + "postinstall": "npx --yes copyfiles -u 3 \"node_modules/@github/copilot-language-server/native/**/*\" \".\" && npx --yes copyfiles -u 3 \"node_modules/@github/copilot-language-server/dist/**/*\" \".\"" }, "dependencies": { "@github/copilot-language-server": "^1.244.0" diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/LsStreamConnectionProvider.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/LsStreamConnectionProvider.java index 50728ac2..e2cb6cd5 100644 --- a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/LsStreamConnectionProvider.java +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/LsStreamConnectionProvider.java @@ -5,15 +5,18 @@ import java.net.URI; import java.net.URISyntaxException; import java.net.URL; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; -import java.util.LinkedList; +import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import org.eclipse.core.runtime.FileLocator; import org.eclipse.core.runtime.URIUtil; import org.eclipse.jdt.annotation.Nullable; import org.eclipse.lsp4e.server.ProcessStreamConnectionProvider; +import org.eclipse.wildwebdeveloper.embedder.node.NodeJSManager; import org.osgi.framework.Bundle; import com.microsoft.copilot.eclipse.core.CopilotCore; @@ -29,7 +32,6 @@ public class LsStreamConnectionProvider extends ProcessStreamConnectionProvider { public static final String EDITOR_NAME = "Eclipse"; - public static final String EDITOR_PLUGIN_NAME = "GitHub Copilot for Eclipse"; @Override @@ -44,25 +46,128 @@ public Object getInitializationOptions(@Nullable URI rootUri) { @Override public void start() throws IOException { - // load lsp binary - // call normalize to remove any relative path components and avoid "FILE_PATH_TOO_LONG" error - Path binary = findBinary().normalize(); + try { + startBinaryLspAgent(); + } catch (Exception e) { + startJsLspAgent(e); + } + CopilotCore.LOGGER.log(LogLevel.INFO, "Lsp agent started successfully."); + } + + private void startBinaryLspAgent() throws IOException { + CopilotCore.LOGGER.log(LogLevel.INFO, "Starting language server with binary lsp agent."); + this.setCommands(getBinaryLspCommands()); + super.start(); + } + + private void startJsLspAgent(Exception e) throws IOException { + CopilotCore.LOGGER.log(LogLevel.ERROR, "Binary LSP agent start failed. Retrying with JS agent.", e); + this.setCommands(getJavaScriptCommands()); + super.start(); + } + + private List getBinaryLspCommands() throws IOException { + Path binary = findAndValidateBinary(); + return buildCommands(binary.toString()); + } + + private Path findAndValidateBinary() throws IOException { + + Path binary = findBinary(); + if (binary == null) { throw new IOException("Could not find the language server binary"); } + // call normalize to remove any relative path components and avoid "FILE_PATH_TOO_LONG" error + binary = binary.normalize(); File executable = binary.toFile(); - if (!executable.canExecute()) { - boolean canExecute = executable.setExecutable(true); - if (!canExecute) { - // TODO: throw error or handle it? + if (!executable.canExecute() && !executable.setExecutable(true)) { + throw new IOException("Could not make the language server binary executable"); + } + + return binary; + } + + private List getJavaScriptCommands() throws IOException { + try { + String nodePath = findNodeAbsolutePath(); + String jsLspPath = findJavaScriptLanguageServerPath(); + + if (nodePath == null) { + throw new IOException("Node path not found"); } + if (jsLspPath == null) { + throw new IOException("JavaScript lsp path not found"); + } + + return buildCommands(nodePath, jsLspPath); + } catch (Exception e) { + CopilotCore.LOGGER.log(LogLevel.ERROR, "Failed to get JavaScript commands. ", e); + throw e; } - List commands = new LinkedList<>(); - commands.add(binary.toString()); + // TODO: In the future, if users have environment variables set up that impact the js server startup, we should + // clear the related environment variables here. Reference: + // https://github.com/microsoft/copilot-intellij/blob/df3fa9e82ddee36342c50b310be321d552238a30/core/src/main/java/com/github/copilot/lang/agent/CopilotAgentCommandLine.java#L45C4-L54C6 + } + + private List buildCommands(String... commandParts) { + List commands = new ArrayList<>(Arrays.asList(commandParts)); commands.add("--stdio"); - this.setCommands(commands); - super.start(); + enforceUtf8Charset(commands); + return commands; + } + + /** + * Enforce UTF-8 charset for the LSP agent commands. + */ + private void enforceUtf8Charset(List commands) { + commands.replaceAll(command -> new String(command.getBytes(StandardCharsets.UTF_8), StandardCharsets.UTF_8)); + } + + private @Nullable String findNodeAbsolutePath() throws IOException { + try { + // The 'wildwebdeveloper' bundle is optional for Eclipse. Ensure it is available before attempting to use it. + Class.forName("org.eclipse.wildwebdeveloper.embedder.node.NodeJSManager"); + } catch (ClassNotFoundException | NoClassDefFoundError e) { + CopilotCore.LOGGER.log(LogLevel.INFO, + "Get JavaScript commands aborted. org.eclipse.wildwebdeveloper.embedder.node.NodeJSManager not found."); + return null; + } + File nodeJsLocation = NodeJSManager.getNodeJsLocation(); + if (nodeJsLocation == null) { + throw new IOException("Failed to find Node.js path"); + } + return nodeJsLocation.getAbsolutePath(); + } + + private @Nullable String findJavaScriptLanguageServerPath() throws IOException { + Path distPath = findAgentDistDirectoryPath(); + + if (distPath == null) { + throw new IOException("Unable to locate dist dir for js language server"); + } + + Path jsFilePath = distPath.resolve("language-server.js"); + if (!Files.exists(jsFilePath)) { + throw new IOException("Unable to locate language-server.js file"); + } + + return jsFilePath.toString(); + } + + private @Nullable Path findAgentDistDirectoryPath() { + URL url = CopilotCore.getPlugin().getBundle().getEntry("copilot-agent/dist"); + if (url == null) { + return null; + } + + try { + return URIUtil.toFile(URIUtil.toURI(FileLocator.toFileURL(url))).toPath(); + } catch (URISyntaxException | IOException e) { + CopilotCore.LOGGER.log(LogLevel.ERROR, e); + return null; + } } private @Nullable Path findBinary() throws IOException { diff --git a/target-platform.target b/target-platform.target index cf1e2950..4de6fa38 100644 --- a/target-platform.target +++ b/target-platform.target @@ -15,6 +15,13 @@ + + + + + From ba931cd46c2c8f8ae7d8d2bbf0a7af138e5c3365 Mon Sep 17 00:00:00 2001 From: yanshudan <1397370237@qq.com> Date: Mon, 30 Dec 2024 16:06:58 +0800 Subject: [PATCH 046/690] feat - Add preference page and auto show completion pref (#46) --- .../copilot/eclipse/core/Constants.java | 1 + .../ui/completion/EditorManagerTests.java | 12 +++-- .../META-INF/MANIFEST.MF | 1 + .../plugin.properties | 3 +- com.microsoft.copilot.eclipse.ui/plugin.xml | 54 ++++++++++++------- .../copilot/eclipse/ui/CopilotUi.java | 6 +-- .../ui/completion/CompletionHandler.java | 27 ++++++++-- .../eclipse/ui/completion/EditorsManager.java | 8 ++- .../copilot/eclipse/ui/i18n/Messages.java | 2 + .../eclipse/ui/i18n/messages.properties | 5 +- .../CopilotPreferenceInitializer.java | 19 +++++++ .../prerferences/CopilotPreferencesPage.java | 41 ++++++++++++++ 12 files changed, 145 insertions(+), 34 deletions(-) create mode 100644 com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/prerferences/CopilotPreferenceInitializer.java create mode 100644 com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/prerferences/CopilotPreferencesPage.java diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/Constants.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/Constants.java index 9894b8e0..95d5479c 100644 --- a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/Constants.java +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/Constants.java @@ -10,4 +10,5 @@ private Constants() { } public static final String PLUGIN_ID = "com.microsoft.copilot.eclipse"; + public static final String AUTO_SHOW_COMPLETION = "enableAutoCompletions"; } diff --git a/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/completion/EditorManagerTests.java b/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/completion/EditorManagerTests.java index 462db6cc..a4338eb8 100644 --- a/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/completion/EditorManagerTests.java +++ b/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/completion/EditorManagerTests.java @@ -4,6 +4,7 @@ import static org.junit.jupiter.api.Assertions.assertNull; import static org.mockito.Mockito.mock; +import org.eclipse.jface.preference.IPreferenceStore; import org.eclipse.ui.texteditor.ITextEditor; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -22,9 +23,12 @@ class EditorManagerTests { @Mock private CompletionProvider mockProvider; + @Mock + private IPreferenceStore mockPreferenceStore; + @Test void testCreateHandlerForNull() { - EditorsManager manager = new EditorsManager(mockServer, mockProvider); + EditorsManager manager = new EditorsManager(mockServer, mockProvider, mockPreferenceStore); assertNull(manager.getOrCreateCompletionHandlerFor(null)); } @@ -33,7 +37,7 @@ void testCreateHandlerForNull() { void testGetOrCreateCompletionHandlerForReturnsNewHandlerWhenNotPresent() { ITextEditor mockEditor = mock(ITextEditor.class); - EditorsManager manager = new EditorsManager(mockServer, mockProvider); + EditorsManager manager = new EditorsManager(mockServer, mockProvider, mockPreferenceStore); CompletionHandler handler = manager.getOrCreateCompletionHandlerFor(mockEditor); assertNotNull(handler); @@ -41,7 +45,7 @@ void testGetOrCreateCompletionHandlerForReturnsNewHandlerWhenNotPresent() { @Test void testGetActiveHandlerWhenNoActiveEditor() { - EditorsManager manager = new EditorsManager(mockServer, mockProvider); + EditorsManager manager = new EditorsManager(mockServer, mockProvider, mockPreferenceStore); assertNull(manager.getActiveCompletionHandler()); } @@ -49,7 +53,7 @@ void testGetActiveHandlerWhenNoActiveEditor() { @Test void testGetActiveHandlerWhenActiveEditor() { ITextEditor mockEditor = mock(ITextEditor.class); - EditorsManager manager = new EditorsManager(mockServer, mockProvider); + EditorsManager manager = new EditorsManager(mockServer, mockProvider, mockPreferenceStore); manager.getOrCreateCompletionHandlerFor(mockEditor); manager.setActiveEditor(mockEditor); diff --git a/com.microsoft.copilot.eclipse.ui/META-INF/MANIFEST.MF b/com.microsoft.copilot.eclipse.ui/META-INF/MANIFEST.MF index 8f68ff26..02653bcf 100644 --- a/com.microsoft.copilot.eclipse.ui/META-INF/MANIFEST.MF +++ b/com.microsoft.copilot.eclipse.ui/META-INF/MANIFEST.MF @@ -9,6 +9,7 @@ Export-Package: com.microsoft.copilot.eclipse.ui, com.microsoft.copilot.eclipse.ui.dialogs, com.microsoft.copilot.eclipse.ui.handlers, com.microsoft.copilot.eclipse.ui.i18n, + com.microsoft.copilot.eclipse.ui.prerferences, com.microsoft.copilot.eclipse.ui.utils Bundle-Activator: com.microsoft.copilot.eclipse.ui.CopilotUi Bundle-RequiredExecutionEnvironment: JavaSE-17 diff --git a/com.microsoft.copilot.eclipse.ui/plugin.properties b/com.microsoft.copilot.eclipse.ui/plugin.properties index 0b65d477..8b7a6b2d 100644 --- a/com.microsoft.copilot.eclipse.ui/plugin.properties +++ b/com.microsoft.copilot.eclipse.ui/plugin.properties @@ -4,4 +4,5 @@ command.discardSuggestion.name=Discard Suggestion command.copilotForEclipsePlugin.name=GitHub Copilot for Eclipse command.signInToGitHub.name=Sign in to GitHub Copilot command.signOutFromGitHub.name=Sign out from GitHub Copilot -command.viewFeedbackForum.name=View Feedback Forum \ No newline at end of file +command.viewFeedbackForum.name=View Feedback Forum +command.preferencesPage.name=GitHub Copilot for Eclipse \ No newline at end of file diff --git a/com.microsoft.copilot.eclipse.ui/plugin.xml b/com.microsoft.copilot.eclipse.ui/plugin.xml index f2786167..dbef6ce2 100644 --- a/com.microsoft.copilot.eclipse.ui/plugin.xml +++ b/com.microsoft.copilot.eclipse.ui/plugin.xml @@ -23,21 +23,35 @@ + + + + + + + + + id="com.microsoft.copilot.eclipse.commands.showStatusBarMenu" + name="%command.copilotForEclipsePlugin.name"> + id="com.microsoft.copilot.eclipse.commands.signIn" + name="%command.signInToGitHub.name"> + id="com.microsoft.copilot.eclipse.commands.signOut" + name="%command.signOutFromGitHub.name"> - - + + @@ -65,12 +79,12 @@ commandId="com.microsoft.copilot.eclipse.commands.showStatusBarMenu"> + class="com.microsoft.copilot.eclipse.ui.handlers.SignInHandler" + commandId="com.microsoft.copilot.eclipse.commands.signIn"> + class="com.microsoft.copilot.eclipse.ui.handlers.SignOutHandler" + commandId="com.microsoft.copilot.eclipse.commands.signOut"> - - + + - + @@ -112,4 +126,4 @@ sequence="ESC"> - + \ No newline at end of file diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/CopilotUi.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/CopilotUi.java index 4db36271..dcb34d98 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/CopilotUi.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/CopilotUi.java @@ -3,12 +3,12 @@ import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.OperationCanceledException; -import org.eclipse.core.runtime.Plugin; import org.eclipse.core.runtime.Status; import org.eclipse.core.runtime.jobs.Job; import org.eclipse.ui.IEditorPart; import org.eclipse.ui.IWorkbenchWindow; import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.plugin.AbstractUIPlugin; import org.osgi.framework.BundleContext; import com.microsoft.copilot.eclipse.core.CopilotCore; @@ -23,7 +23,7 @@ /** * The plug-in runtime class for the Copilot plug-in containing the UI support, like dialogs, ghost text rendering, etc. */ -public class CopilotUi extends Plugin { +public class CopilotUi extends AbstractUIPlugin { private static CopilotUi COPILOT_UI_PLUGIN = null; @@ -62,7 +62,7 @@ protected IStatus run(IProgressMonitor monitor) { } CopilotUi.this.editorsManager = new EditorsManager(connection, - CopilotCore.getPlugin().getCompletionProvider()); + CopilotCore.getPlugin().getCompletionProvider(), getPreferenceStore()); CopilotUi.this.editorLifecycleListener = new EditorLifecycleListener(editorsManager); CopilotUi.this.completionStatusManager = new CompletionStatusManager(); diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionHandler.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionHandler.java index 75749bef..e2bfc4e8 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionHandler.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionHandler.java @@ -3,6 +3,7 @@ import java.io.IOException; import java.net.URI; +import org.eclipse.jface.preference.IPreferenceStore; import org.eclipse.jface.text.BadLocationException; import org.eclipse.jface.text.BadPositionCategoryException; import org.eclipse.jface.text.DefaultPositionUpdater; @@ -10,11 +11,14 @@ import org.eclipse.jface.text.ITextOperationTarget; import org.eclipse.jface.text.ITextViewer; import org.eclipse.jface.text.TextSelection; +import org.eclipse.jface.util.IPropertyChangeListener; +import org.eclipse.jface.util.PropertyChangeEvent; import org.eclipse.lsp4e.LSPEclipseUtils; import org.eclipse.swt.custom.CaretEvent; import org.eclipse.swt.custom.CaretListener; import org.eclipse.ui.texteditor.ITextEditor; +import com.microsoft.copilot.eclipse.core.Constants; import com.microsoft.copilot.eclipse.core.completion.CompletionCollection; import com.microsoft.copilot.eclipse.core.completion.CompletionProvider; import com.microsoft.copilot.eclipse.core.logger.LogLevel; @@ -27,7 +31,7 @@ * A class to listen events which are completion related and notify the completion manager to render the ghost text or * apply the suggestion to document. */ -public class CompletionHandler implements CaretListener { +public class CompletionHandler implements CaretListener, IPropertyChangeListener { private CopilotLanguageServerConnection lsConnection; private CompletionProvider provider; @@ -39,12 +43,14 @@ public class CompletionHandler implements CaretListener { private DefaultPositionUpdater positionUpdater; private CompletionManager completionManager; + private boolean autoShowCompletion; + private IPreferenceStore preferenceStore; /** * Creates a new completion handler. */ public CompletionHandler(CopilotLanguageServerConnection lsConnection, CompletionProvider provider, - ITextEditor editor) { + ITextEditor editor, IPreferenceStore preferenceStore) { this.lsConnection = lsConnection; this.textViewer = (ITextViewer) editor.getAdapter(ITextOperationTarget.class); // if the text viewer is null, we will not register listeners. @@ -82,6 +88,11 @@ public CompletionHandler(CopilotLanguageServerConnection lsConnection, Completio this.positionUpdater = new DefaultPositionUpdater(this.getCategory()); this.document.addPositionCategory(this.getCategory()); this.document.addPositionUpdater(this.positionUpdater); + + // initialize the auto show completion preference and add listener to update it. + this.preferenceStore = preferenceStore; + this.autoShowCompletion = preferenceStore.getBoolean(Constants.AUTO_SHOW_COMPLETION); + preferenceStore.addPropertyChangeListener(this); } /** @@ -154,11 +165,20 @@ public void caretMoved(CaretEvent event) { clearCompletionRendering(); } else { this.documentVersion = currentVersion; - triggerCompletion(); + if (this.autoShowCompletion) { + triggerCompletion(); + } } } + @Override + public void propertyChange(PropertyChangeEvent event) { + if (event.getProperty().equals(Constants.AUTO_SHOW_COMPLETION)) { + this.autoShowCompletion = Boolean.parseBoolean(event.getNewValue().toString()); + } + } + /** * Get category for the position updater of this document. */ @@ -177,6 +197,7 @@ public void dispose() { this.completionManager.dispose(); this.completionManager = null; + preferenceStore.removePropertyChangeListener(this); lsConnection.disconnectDocument(this.documentUri); try { this.document.removePositionCategory(this.getCategory()); diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/EditorsManager.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/EditorsManager.java index 25870934..6a042971 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/EditorsManager.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/EditorsManager.java @@ -5,6 +5,7 @@ import java.util.concurrent.atomic.AtomicReference; import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jface.preference.IPreferenceStore; import org.eclipse.ui.texteditor.ITextEditor; import com.microsoft.copilot.eclipse.core.completion.CompletionProvider; @@ -19,15 +20,18 @@ public class EditorsManager { private CompletionProvider completionProvider; private Map editorMap; private AtomicReference activeEditor; + private IPreferenceStore preferenceStore; /** * Creates a new EditorManager. */ - public EditorsManager(CopilotLanguageServerConnection languageServer, CompletionProvider completionProvider) { + public EditorsManager(CopilotLanguageServerConnection languageServer, CompletionProvider completionProvider, + IPreferenceStore preferenceStore) { this.languageServer = languageServer; this.completionProvider = completionProvider; this.editorMap = new ConcurrentHashMap<>(); this.activeEditor = new AtomicReference<>(); + this.preferenceStore = preferenceStore; } /** @@ -41,7 +45,7 @@ public CompletionHandler getOrCreateCompletionHandlerFor(ITextEditor editor) { } return editorMap.computeIfAbsent(editor, - edt -> new CompletionHandler(this.languageServer, this.completionProvider, edt)); + edt -> new CompletionHandler(this.languageServer, this.completionProvider, edt, this.preferenceStore)); } /** diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/i18n/Messages.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/i18n/Messages.java index 02f42a04..62924551 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/i18n/Messages.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/i18n/Messages.java @@ -41,6 +41,8 @@ public final class Messages extends NLS { public static String signOutHandler_msgDialog_signOutSuccess; public static String signOutHandler_msgDialog_signOutFailed; public static String signOutHandler_msgDialog_signOutFailedFailure; + public static String preferencesPage_description; + public static String preferencesPage_autoShowCompletion; static { // initialize resource bundle diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/i18n/messages.properties b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/i18n/messages.properties index 52e9adb9..99559d83 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/i18n/messages.properties +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/i18n/messages.properties @@ -31,4 +31,7 @@ signInHandler_msgDialog_signInFailedFailure=Copilot Sign In Failure signOutHandler_msgDialog_githubCopilot=GitHub Copilot signOutHandler_msgDialog_signOutSuccess=You have successfully signed out from Copilot. signOutHandler_msgDialog_signOutFailed=Unable to sign out to GitHub Copilot at this time -signOutHandler_msgDialog_signOutFailedFailure=Copilot Sign Out Failure \ No newline at end of file +signOutHandler_msgDialog_signOutFailedFailure=Copilot Sign Out Failure + +preferencesPage_description=Configure GitHub Copilot for Eclipse +preferencesPage_autoShowCompletion=Automatically show inline completions \ No newline at end of file diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/prerferences/CopilotPreferenceInitializer.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/prerferences/CopilotPreferenceInitializer.java new file mode 100644 index 00000000..b1ea307a --- /dev/null +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/prerferences/CopilotPreferenceInitializer.java @@ -0,0 +1,19 @@ +package com.microsoft.copilot.eclipse.ui.prerferences; + +import org.eclipse.core.runtime.preferences.AbstractPreferenceInitializer; +import org.eclipse.jface.preference.IPreferenceStore; + +import com.microsoft.copilot.eclipse.core.Constants; +import com.microsoft.copilot.eclipse.ui.CopilotUi; + +/** + * A class to initialize the default preferences for the plugin. + */ +public class CopilotPreferenceInitializer extends AbstractPreferenceInitializer { + + @Override + public void initializeDefaultPreferences() { + IPreferenceStore pref = CopilotUi.getPlugin().getPreferenceStore(); + pref.setDefault(Constants.AUTO_SHOW_COMPLETION, true); + } +} diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/prerferences/CopilotPreferencesPage.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/prerferences/CopilotPreferencesPage.java new file mode 100644 index 00000000..65ddf1eb --- /dev/null +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/prerferences/CopilotPreferencesPage.java @@ -0,0 +1,41 @@ +package com.microsoft.copilot.eclipse.ui.prerferences; + +import org.eclipse.core.runtime.preferences.InstanceScope; +import org.eclipse.jface.preference.BooleanFieldEditor; +import org.eclipse.jface.preference.FieldEditorPreferencePage; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.ui.IWorkbench; +import org.eclipse.ui.IWorkbenchPreferencePage; +import org.eclipse.ui.preferences.ScopedPreferenceStore; + +import com.microsoft.copilot.eclipse.core.Constants; +import com.microsoft.copilot.eclipse.ui.CopilotUi; +import com.microsoft.copilot.eclipse.ui.i18n.Messages; + +/** + * This class is used to create the preference page for the plugin. + */ +public class CopilotPreferencesPage extends FieldEditorPreferencePage implements IWorkbenchPreferencePage { + + /** + * Constructor. + */ + public CopilotPreferencesPage() { + super(GRID); + } + + @Override + public void createFieldEditors() { + var parent = getFieldEditorParent(); + var editorGroup = new Composite(parent, 0); + addField(new BooleanFieldEditor(Constants.AUTO_SHOW_COMPLETION, Messages.preferencesPage_autoShowCompletion, + (Composite) editorGroup)); + } + + @Override + public void init(IWorkbench workbench) { + // second parameter is typically the plug-in id + setPreferenceStore(CopilotUi.getPlugin().getPreferenceStore()); + } + +} \ No newline at end of file From c4a8e19258767351e1891353b0dfe85818ddf5c0 Mon Sep 17 00:00:00 2001 From: ethanyhou <149548697+ethanyhou@users.noreply.github.com> Date: Mon, 30 Dec 2024 19:45:52 +0800 Subject: [PATCH 047/690] fix - Add signin signout listener and fix auth related bugs. (#76) --- ...Tests.java => AuthStatusManagerTests.java} | 18 +-- .../core/completion/CompletionJobTests.java | 34 ++++ .../completion/CompletionProviderTests.java | 4 +- ...tusManager.java => AuthStatusManager.java} | 73 ++++++--- .../core/CopilotAuthStatusListener.java | 14 ++ .../copilot/eclipse/core/CopilotCore.java | 12 +- .../core/completion/CompletionProvider.java | 7 +- .../lsp/CopilotLanguageServerConnection.java | 8 +- ...Manager.java => CopilotStatusManager.java} | 13 +- .../copilot/eclipse/ui/CopilotUi.java | 25 ++- .../ui/dialogs/SignInConfirmDialog.java | 2 +- .../eclipse/ui/handlers/CopilotHandler.java | 6 +- .../ui/handlers/ShowStatusBarMenuHandler.java | 22 +-- .../eclipse/ui/handlers/SignInHandler.java | 147 +++++++++++------- .../eclipse/ui/handlers/SignOutHandler.java | 10 +- 15 files changed, 262 insertions(+), 133 deletions(-) rename com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/{CopilotStatusManagerTests.java => AuthStatusManagerTests.java} (78%) rename com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/{CopilotStatusManager.java => AuthStatusManager.java} (55%) create mode 100644 com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/CopilotAuthStatusListener.java rename com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/{completion/CompletionStatusManager.java => CopilotStatusManager.java} (58%) diff --git a/com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/CopilotStatusManagerTests.java b/com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/AuthStatusManagerTests.java similarity index 78% rename from com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/CopilotStatusManagerTests.java rename to com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/AuthStatusManagerTests.java index dabf9401..d633290f 100644 --- a/com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/CopilotStatusManagerTests.java +++ b/com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/AuthStatusManagerTests.java @@ -16,15 +16,15 @@ import com.microsoft.copilot.eclipse.core.lsp.protocol.CopilotStatusResult; @ExtendWith(MockitoExtension.class) -class CopilotStatusManagerTests { +class AuthStatusManagerTests { @Mock CopilotLanguageServerConnection mockConnection; - CopilotStatusManager copilotStatusManager; + AuthStatusManager authStatusManager; @BeforeEach public void setUp() { - copilotStatusManager = new CopilotStatusManager(mockConnection); + authStatusManager = new AuthStatusManager(mockConnection); } @Test @@ -33,9 +33,9 @@ void testCopilotStatusResultOnSuccess() { expectedResult.setStatus(CopilotStatusResult.OK); when(mockConnection.checkStatus(false)).thenReturn(CompletableFuture.completedFuture(expectedResult)); - copilotStatusManager.checkStatus(); + authStatusManager.checkStatus(); - assertEquals(CopilotStatusResult.OK, copilotStatusManager.getCopilotStatus()); + assertEquals(CopilotStatusResult.OK, authStatusManager.getCopilotStatus()); } @Test @@ -49,10 +49,10 @@ void testCheckStatusOK() throws InterruptedException { when(mockConnection.checkStatus(false)).thenReturn(future); future.complete(expectedResult); - copilotStatusManager.checkStatus(); + authStatusManager.checkStatus(); // Assert final status is OK - assertEquals(CopilotStatusResult.OK, copilotStatusManager.getCopilotStatus()); + assertEquals(CopilotStatusResult.OK, authStatusManager.getCopilotStatus()); } @Test @@ -62,9 +62,9 @@ void testCheckStatusError() { when(mockConnection.checkStatus(false)).thenReturn(future); - copilotStatusManager.checkStatus(); + authStatusManager.checkStatus(); - assertEquals(CopilotStatusResult.ERROR, copilotStatusManager.getCopilotStatus()); + assertEquals(CopilotStatusResult.ERROR, authStatusManager.getCopilotStatus()); } } diff --git a/com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/completion/CompletionJobTests.java b/com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/completion/CompletionJobTests.java index 8a7493ec..fe58f5d7 100644 --- a/com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/completion/CompletionJobTests.java +++ b/com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/completion/CompletionJobTests.java @@ -1,11 +1,13 @@ package com.microsoft.copilot.eclipse.core.completion; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import org.eclipse.core.runtime.IStatus; @@ -19,10 +21,13 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.junit.jupiter.MockitoExtension; +import com.microsoft.copilot.eclipse.core.AuthStatusManager; import com.microsoft.copilot.eclipse.core.completion.CompletionProvider.CompletionJob; import com.microsoft.copilot.eclipse.core.lsp.CopilotLanguageServerConnection; import com.microsoft.copilot.eclipse.core.lsp.protocol.CompletionDocument; import com.microsoft.copilot.eclipse.core.lsp.protocol.CompletionParams; +import com.microsoft.copilot.eclipse.core.lsp.protocol.CompletionResult; +import com.microsoft.copilot.eclipse.core.lsp.protocol.CopilotStatusResult; @ExtendWith(MockitoExtension.class) class CompletionJobTests { @@ -81,5 +86,34 @@ void testShouldTimeoutWhenCompletionTakesTooLong() throws Exception { assertEquals(Status.CANCEL_STATUS, job.getResult()); } + + + @Test + void testTriggerCompletionJobWhenCopilotIsSignedOutNotUsingEclipse() throws InterruptedException { + CopilotStatusResult expectedResult = new CopilotStatusResult(); + expectedResult.setStatus(CopilotStatusResult.ERROR); + AuthStatusManager authStatusManager = mock(AuthStatusManager.class); + when(authStatusManager.setCopilotStatus(CopilotStatusResult.ERROR)).thenReturn(expectedResult); + when(authStatusManager.getCopilotStatus()).thenReturn(CopilotStatusResult.ERROR); + CompletableFuture future = new CompletableFuture<>(); + future.completeExceptionally(new ExecutionException("Not signed in", new Throwable())); + when(mockLsConnection.getCompletions(any())).thenReturn(future); + + CompletionJob job = new CompletionProvider(mockLsConnection, authStatusManager).new CompletionJob(mockLsConnection); + Position position = new Position(0, 0); + CompletionDocument completionDoc = new CompletionDocument("file://test.java", position); + completionDoc.setVersion(1); + completionDoc.setInsertSpaces(true); + completionDoc.setTabSize(4); + job.setCompletionParams(new CompletionParams(completionDoc)); + job.schedule(); + + IJobManager jobManager = Job.getJobManager(); + jobManager.join(CompletionProvider.COMPLETION_JOB_FAMILY, new NullProgressMonitor()); + + assertTrue(job.getResult().getMessage().contains("Not signed in")); + assertEquals(IStatus.ERROR, job.getResult().getSeverity()); + assertEquals(CopilotStatusResult.ERROR, authStatusManager.getCopilotStatus()); + } } diff --git a/com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/completion/CompletionProviderTests.java b/com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/completion/CompletionProviderTests.java index 48826859..b95ea40d 100644 --- a/com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/completion/CompletionProviderTests.java +++ b/com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/completion/CompletionProviderTests.java @@ -24,7 +24,7 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import com.microsoft.copilot.eclipse.core.CopilotStatusManager; +import com.microsoft.copilot.eclipse.core.AuthStatusManager; import com.microsoft.copilot.eclipse.core.lsp.CopilotLanguageServerConnection; import com.microsoft.copilot.eclipse.core.lsp.protocol.CompletionDocument; import com.microsoft.copilot.eclipse.core.lsp.protocol.CompletionItem; @@ -39,7 +39,7 @@ class CompletionProviderTests { private CopilotLanguageServerConnection mockLsConnection; @Mock - private CopilotStatusManager mockStatusManager; + private AuthStatusManager mockStatusManager; @Mock private CompletionListener mockListener; diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/CopilotStatusManager.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/AuthStatusManager.java similarity index 55% rename from com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/CopilotStatusManager.java rename to com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/AuthStatusManager.java index 7f379ea8..7ed3a1ab 100644 --- a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/CopilotStatusManager.java +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/AuthStatusManager.java @@ -1,5 +1,8 @@ package com.microsoft.copilot.eclipse.core; +import java.util.LinkedHashSet; +import java.util.Objects; +import java.util.Set; import java.util.concurrent.ExecutionException; import com.microsoft.copilot.eclipse.core.logger.LogLevel; @@ -10,21 +13,22 @@ /** * Manager for the authentication status. */ -public class CopilotStatusManager { +public class AuthStatusManager { private CopilotLanguageServerConnection connection; - + private Set copilotAuthStatusListeners; private CopilotStatusResult copilotStatusResult; /** - * Constructor for the CopilotStatusManager. + * Constructor for the AuthStatusManager. * * @param connection the connection to the language server. */ - public CopilotStatusManager(CopilotLanguageServerConnection connection) { + public AuthStatusManager(CopilotLanguageServerConnection connection) { this.connection = connection; + this.copilotAuthStatusListeners = new LinkedHashSet<>(); this.copilotStatusResult = new CopilotStatusResult(); - this.copilotStatusResult.setStatus(CopilotStatusResult.OK); + setCopilotStatus(CopilotStatusResult.LOADING); } /** @@ -36,8 +40,9 @@ public CopilotStatusManager(CopilotLanguageServerConnection connection) { public SignInInitiateResult signInInitiate() throws InterruptedException, ExecutionException { SignInInitiateResult result = connection.signInInitiate().get(); if (result.isAlreadySignedIn()) { - this.copilotStatusResult.setStatus(CopilotStatusResult.OK); + setCopilotStatus(CopilotStatusResult.OK); } + return result; } @@ -50,9 +55,10 @@ public SignInInitiateResult signInInitiate() throws InterruptedException, Execut public CopilotStatusResult signInConfirm(String userCode) throws InterruptedException, ExecutionException { CopilotStatusResult result = connection.signInConfirm(userCode).get(); if (result.isSignedIn()) { - this.copilotStatusResult.setStatus(CopilotStatusResult.OK); this.copilotStatusResult.setUser(result.getUser()); } + + setCopilotStatus(result.getStatus()); return result; } @@ -64,30 +70,33 @@ public CopilotStatusResult signInConfirm(String userCode) throws InterruptedExce */ public CopilotStatusResult signOut() throws InterruptedException, ExecutionException { CopilotStatusResult result = connection.signOut().get(); - if (!result.isSignedIn()) { - this.copilotStatusResult.setStatus(CopilotStatusResult.NOT_SIGNED_IN); - } + setCopilotStatus(result.getStatus()); return result; } - + /** - * Set the status to OK. + * Set the CopilotStatusResult string to the given status and notify the listeners. */ - public CopilotStatusResult setCompletionDone() { - this.copilotStatusResult.setStatus(CopilotStatusResult.OK); + public CopilotStatusResult setCopilotStatus(String newCopilotStatusResult) { + if (!Objects.equals(this.copilotStatusResult.getStatus(), newCopilotStatusResult)) { + this.copilotStatusResult.setStatus(newCopilotStatusResult); + onDidCopilotStatusChange(this.copilotStatusResult); + } return this.copilotStatusResult; } /** - * Check the login status for current machine. + * Check the authentication status for current machine. */ public void checkStatus() { - this.connection.checkStatus(false).thenAccept(result -> { - this.copilotStatusResult = result; - }).exceptionally(ex -> { - CopilotCore.LOGGER.log(LogLevel.ERROR, ex); - this.copilotStatusResult.setStatus(CopilotStatusResult.ERROR); - + this.connection.checkStatus(false).handle((result, ex) -> { + if (ex != null) { + CopilotCore.LOGGER.log(LogLevel.ERROR, ex); + setCopilotStatus(CopilotStatusResult.ERROR); + } else { + setCopilotStatus(result.getStatus()); + } + onDidCopilotStatusChange(this.copilotStatusResult); return null; }); } @@ -101,4 +110,26 @@ public String getCopilotStatus() { } return this.copilotStatusResult.getStatus(); } + + /** + * Add a listener for the authentication status. + */ + public void addCopilotAuthStatusListener(CopilotAuthStatusListener listener) { + this.copilotAuthStatusListeners.add(listener); + } + + /** + * Remove the listener for the authentication status. + */ + public void removeCopilotAuthStatusListener(CopilotAuthStatusListener listener) { + this.copilotAuthStatusListeners.remove(listener); + } + + private void onDidCopilotStatusChange(CopilotStatusResult copilotStatusResult) { + if (!this.copilotAuthStatusListeners.isEmpty()) { + for (CopilotAuthStatusListener listener : this.copilotAuthStatusListeners) { + listener.onDidCopilotStatusChange(copilotStatusResult); + } + } + } } diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/CopilotAuthStatusListener.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/CopilotAuthStatusListener.java new file mode 100644 index 00000000..b63f966a --- /dev/null +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/CopilotAuthStatusListener.java @@ -0,0 +1,14 @@ +package com.microsoft.copilot.eclipse.core; + +import com.microsoft.copilot.eclipse.core.lsp.protocol.CopilotStatusResult; + +/** + * Listener for the authentication status. + */ +public interface CopilotAuthStatusListener { + + /** + * Notifies to the listeners when the authentication status is changed. + */ + void onDidCopilotStatusChange(CopilotStatusResult copilotStatusResult); +} diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/CopilotCore.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/CopilotCore.java index aeb37af4..cad2f071 100644 --- a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/CopilotCore.java +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/CopilotCore.java @@ -24,7 +24,7 @@ public class CopilotCore extends Plugin { private CopilotLanguageServerConnection copilotLanguageServer; - private CopilotStatusManager copilotStatusManager; + private AuthStatusManager authStatusManager; private CompletionProvider completionProvider; private static CopilotCore COPILOT_CORE_PLUGIN = null; @@ -74,10 +74,10 @@ void init() { LanguageServerWrapper wrapper = LanguageServiceAccessor.startLanguageServer(serverDef); this.copilotLanguageServer = new CopilotLanguageServerConnection(wrapper); - this.copilotStatusManager = new CopilotStatusManager(this.copilotLanguageServer); - this.completionProvider = new CompletionProvider(this.copilotLanguageServer, copilotStatusManager); + this.authStatusManager = new AuthStatusManager(this.copilotLanguageServer); + this.completionProvider = new CompletionProvider(this.copilotLanguageServer, authStatusManager); - this.copilotStatusManager.checkStatus(); + this.authStatusManager.checkStatus(); }; Job initJob = new Job("GitHub Copilot Initialization...") { @@ -99,8 +99,8 @@ public CopilotLanguageServerConnection getCopilotLanguageServer() { return copilotLanguageServer; } - public CopilotStatusManager getCopilotStatusManager() { - return copilotStatusManager; + public AuthStatusManager getAuthStatusManager() { + return authStatusManager; } public CompletionProvider getCompletionProvider() { diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/completion/CompletionProvider.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/completion/CompletionProvider.java index cc0fa36d..15fae504 100644 --- a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/completion/CompletionProvider.java +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/completion/CompletionProvider.java @@ -13,9 +13,9 @@ import org.eclipse.core.runtime.jobs.Job; import org.eclipse.lsp4j.Position; +import com.microsoft.copilot.eclipse.core.AuthStatusManager; import com.microsoft.copilot.eclipse.core.Constants; import com.microsoft.copilot.eclipse.core.CopilotCore; -import com.microsoft.copilot.eclipse.core.CopilotStatusManager; import com.microsoft.copilot.eclipse.core.logger.LogLevel; import com.microsoft.copilot.eclipse.core.lsp.CopilotLanguageServerConnection; import com.microsoft.copilot.eclipse.core.lsp.protocol.CompletionDocument; @@ -36,12 +36,12 @@ public class CompletionProvider { private CompletionJob completionJob; private Set completionListeners; private Set completionStatusListeners; - private CopilotStatusManager statusManager; + private AuthStatusManager statusManager; /** * Creates a new completion provider. */ - public CompletionProvider(CopilotLanguageServerConnection lsConnection, CopilotStatusManager statusManager) { + public CompletionProvider(CopilotLanguageServerConnection lsConnection, AuthStatusManager statusManager) { this.statusManager = statusManager; this.completionJob = new CompletionJob(lsConnection); this.completionListeners = new LinkedHashSet<>(); @@ -160,6 +160,7 @@ private IStatus runCompletion(IProgressMonitor monitor) { } catch (InterruptedException e) { return Status.CANCEL_STATUS; } catch (ExecutionException e) { + statusManager.setCopilotStatus(CopilotStatusResult.ERROR); CopilotCore.LOGGER.log(LogLevel.ERROR, e); return new Status(IStatus.ERROR, Constants.PLUGIN_ID, e.getMessage(), e); } catch (TimeoutException e) { diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/CopilotLanguageServerConnection.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/CopilotLanguageServerConnection.java index c565a8bf..2b7078d1 100644 --- a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/CopilotLanguageServerConnection.java +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/CopilotLanguageServerConnection.java @@ -9,7 +9,7 @@ import org.eclipse.lsp4e.LanguageServerWrapper; import org.eclipse.lsp4j.services.LanguageServer; -import com.microsoft.copilot.eclipse.core.CopilotStatusManager; +import com.microsoft.copilot.eclipse.core.AuthStatusManager; import com.microsoft.copilot.eclipse.core.lsp.protocol.CheckStatusParams; import com.microsoft.copilot.eclipse.core.lsp.protocol.CompletionParams; import com.microsoft.copilot.eclipse.core.lsp.protocol.CompletionResult; @@ -84,7 +84,7 @@ public CompletableFuture getCompletions(CompletionParams param } /** - * Please use the {@link CopilotStatusManager#signInInitiate()} method instead. + * Please use the {@link AuthStatusManager#signInInitiate()} method instead. *

* Initiate the sign in process. */ @@ -95,7 +95,7 @@ public CompletableFuture signInInitiate() { } /** - * Please use the {@link CopilotStatusManager#signInConfirm()} method instead. + * Please use the {@link AuthStatusManager#signInConfirm()} method instead. *

* Confirm the sign in process. */ @@ -108,7 +108,7 @@ public CompletableFuture signInConfirm(String userCode) { } /** - * Please use the {@link CopilotStatusManager#signOut()} method instead. + * Please use the {@link AuthStatusManager#signOut()} method instead. *

* Sign out from the GitHub Copilot. */ diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionStatusManager.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/CopilotStatusManager.java similarity index 58% rename from com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionStatusManager.java rename to com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/CopilotStatusManager.java index 30ef37c1..df1ea8f5 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionStatusManager.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/CopilotStatusManager.java @@ -1,19 +1,21 @@ -package com.microsoft.copilot.eclipse.ui.completion; +package com.microsoft.copilot.eclipse.ui; +import com.microsoft.copilot.eclipse.core.CopilotAuthStatusListener; import com.microsoft.copilot.eclipse.core.completion.CompletionStatusListener; +import com.microsoft.copilot.eclipse.core.lsp.protocol.CopilotStatusResult; import com.microsoft.copilot.eclipse.ui.utils.UiUtils; /** * Listener for tracking copilot completion status. */ -public class CompletionStatusManager implements CompletionStatusListener { +public class CopilotStatusManager implements CompletionStatusListener, CopilotAuthStatusListener { private boolean completionInProgress; /** * Constructor for the CompletionStatusManager. */ - public CompletionStatusManager() { + public CopilotStatusManager() { } @Override @@ -31,4 +33,9 @@ public void onCompletionDone() { public boolean isCompletionInProgress() { return completionInProgress; } + + @Override + public void onDidCopilotStatusChange(CopilotStatusResult copilotStatusResult) { + UiUtils.refreshCopilotMenu(); + } } \ No newline at end of file diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/CopilotUi.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/CopilotUi.java index dcb34d98..84dbfd49 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/CopilotUi.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/CopilotUi.java @@ -15,7 +15,6 @@ import com.microsoft.copilot.eclipse.core.logger.CopilotForEclipseLogger; import com.microsoft.copilot.eclipse.core.logger.LogLevel; import com.microsoft.copilot.eclipse.core.lsp.CopilotLanguageServerConnection; -import com.microsoft.copilot.eclipse.ui.completion.CompletionStatusManager; import com.microsoft.copilot.eclipse.ui.completion.EditorLifecycleListener; import com.microsoft.copilot.eclipse.ui.completion.EditorsManager; import com.microsoft.copilot.eclipse.ui.utils.SwtUtils; @@ -27,7 +26,7 @@ public class CopilotUi extends AbstractUIPlugin { private static CopilotUi COPILOT_UI_PLUGIN = null; - private CompletionStatusManager completionStatusManager; + private CopilotStatusManager copilotStatusManager; private EditorLifecycleListener editorLifecycleListener; private EditorsManager editorsManager; public static final CopilotForEclipseLogger LOGGER = new CopilotForEclipseLogger(CopilotCore.class.getName()); @@ -64,10 +63,11 @@ protected IStatus run(IProgressMonitor monitor) { CopilotUi.this.editorsManager = new EditorsManager(connection, CopilotCore.getPlugin().getCompletionProvider(), getPreferenceStore()); CopilotUi.this.editorLifecycleListener = new EditorLifecycleListener(editorsManager); - CopilotUi.this.completionStatusManager = new CompletionStatusManager(); + CopilotUi.this.copilotStatusManager = new CopilotStatusManager(); registerPartListener(); addCompletionStatusListener(); + addCopilotAuthStatusListener(); // Initialize the completion handler for the active editor in case we miss the event // to initialize it. @@ -83,14 +83,16 @@ protected IStatus run(IProgressMonitor monitor) { initJob.schedule(); } - public CompletionStatusManager getCompletionStatusManager() { - return completionStatusManager; + public CopilotStatusManager getAuthAndCompletionStatusManager() { + return copilotStatusManager; } @Override public void stop(BundleContext context) throws Exception { unregisterPartListener(); removeCompletionStatusListener(); + removeCopilotAuthStatusListener(); + if (this.editorsManager != null) { this.editorsManager.dispose(); } @@ -107,8 +109,12 @@ private void registerPartListener() { } } + private void addCopilotAuthStatusListener() { + CopilotCore.getPlugin().getAuthStatusManager().addCopilotAuthStatusListener(this.copilotStatusManager); + } + private void addCompletionStatusListener() { - CopilotCore.getPlugin().getCompletionProvider().addCompletionStatusListener(this.completionStatusManager); + CopilotCore.getPlugin().getCompletionProvider().addCompletionStatusListener(this.copilotStatusManager); } private void initCompletionHandlerForActiveEditor() { @@ -126,7 +132,12 @@ private void unregisterPartListener() { } private void removeCompletionStatusListener() { - CopilotCore.getPlugin().getCompletionProvider().removeCompletionStatusListener(this.completionStatusManager); + CopilotCore.getPlugin().getCompletionProvider().removeCompletionStatusListener(this.copilotStatusManager); + } + + private void removeCopilotAuthStatusListener() { + CopilotCore.getPlugin().getAuthStatusManager() + .removeCopilotAuthStatusListener(this.copilotStatusManager); } } diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/dialogs/SignInConfirmDialog.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/dialogs/SignInConfirmDialog.java index c5558fc1..c28b40a6 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/dialogs/SignInConfirmDialog.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/dialogs/SignInConfirmDialog.java @@ -75,7 +75,7 @@ public void run(IProgressMonitor monitor) throws InvocationTargetException, Inte try { future = CompletableFuture.supplyAsync(() -> { try { - return CopilotCore.getPlugin().getCopilotStatusManager().signInConfirm(userCode); + return CopilotCore.getPlugin().getAuthStatusManager().signInConfirm(userCode); } catch (Exception e) { CopilotUi.LOGGER.log(LogLevel.ERROR, e); return null; diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/CopilotHandler.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/CopilotHandler.java index b3692fea..be347e3b 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/CopilotHandler.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/CopilotHandler.java @@ -5,9 +5,9 @@ import com.microsoft.copilot.eclipse.core.CopilotCore; import com.microsoft.copilot.eclipse.core.lsp.CopilotLanguageServerConnection; +import com.microsoft.copilot.eclipse.ui.CopilotStatusManager; import com.microsoft.copilot.eclipse.ui.CopilotUi; import com.microsoft.copilot.eclipse.ui.completion.CompletionHandler; -import com.microsoft.copilot.eclipse.ui.completion.CompletionStatusManager; import com.microsoft.copilot.eclipse.ui.completion.EditorsManager; /** @@ -34,7 +34,7 @@ public CopilotLanguageServerConnection getLanguageServerConnection() { return CopilotCore.getPlugin().getCopilotLanguageServer(); } - public CompletionStatusManager getCompletionStatusManager() { - return CopilotUi.getPlugin().getCompletionStatusManager(); + public CopilotStatusManager getAuthAndCompletionStatusManager() { + return CopilotUi.getPlugin().getAuthAndCompletionStatusManager(); } } diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/ShowStatusBarMenuHandler.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/ShowStatusBarMenuHandler.java index 5ce7a299..43e0dcf6 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/ShowStatusBarMenuHandler.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/ShowStatusBarMenuHandler.java @@ -21,12 +21,12 @@ import org.eclipse.ui.handlers.IHandlerService; import org.eclipse.ui.menus.UIElement; +import com.microsoft.copilot.eclipse.core.AuthStatusManager; import com.microsoft.copilot.eclipse.core.CopilotCore; -import com.microsoft.copilot.eclipse.core.CopilotStatusManager; import com.microsoft.copilot.eclipse.core.logger.LogLevel; import com.microsoft.copilot.eclipse.core.lsp.protocol.CopilotStatusResult; +import com.microsoft.copilot.eclipse.ui.CopilotStatusManager; import com.microsoft.copilot.eclipse.ui.CopilotUi; -import com.microsoft.copilot.eclipse.ui.completion.CompletionStatusManager; import com.microsoft.copilot.eclipse.ui.i18n.Messages; import com.microsoft.copilot.eclipse.ui.utils.UiUtils; @@ -35,13 +35,13 @@ */ public class ShowStatusBarMenuHandler extends CopilotHandler implements IElementUpdater { private IHandlerService handlerService; - private CopilotStatusManager copilotStatusManager; + private AuthStatusManager authStatusManager; private SpinnerJob spinnerJob; @Override public Object execute(ExecutionEvent event) throws ExecutionException { handlerService = HandlerUtil.getActiveWorkbenchWindow(event).getService(IHandlerService.class); - copilotStatusManager = CopilotCore.getPlugin().getCopilotStatusManager(); + authStatusManager = CopilotCore.getPlugin().getAuthStatusManager(); MenuManager menuManager = new MenuManager(); // Sign in status section @@ -54,7 +54,7 @@ public Object execute(ExecutionEvent event) throws ExecutionException { // Sign in & sign out section menuManager.add(new Separator()); - if (!Objects.equals(copilotStatusManager.getCopilotStatus(), CopilotStatusResult.LOADING)) { + if (!Objects.equals(authStatusManager.getCopilotStatus(), CopilotStatusResult.LOADING)) { addSignInOrSignOutAction(menuManager); } @@ -66,7 +66,7 @@ public Object execute(ExecutionEvent event) throws ExecutionException { @Override public void updateElement(UIElement element, Map parameters) { - CompletionStatusManager completionStatusManager = getCompletionStatusManager(); + CopilotStatusManager completionStatusManager = getAuthAndCompletionStatusManager(); if (completionStatusManager.isCompletionInProgress()) { scheduleSpinnerJob(element); @@ -76,7 +76,7 @@ public void updateElement(UIElement element, Map parameters) { spinnerJob.cancel(); } - String copilotStatus = CopilotCore.getPlugin().getCopilotStatusManager().getCopilotStatus(); + String copilotStatus = CopilotCore.getPlugin().getAuthStatusManager().getCopilotStatus(); String iconPath = null; switch (copilotStatus) { @@ -104,7 +104,7 @@ public void updateElement(UIElement element, Map parameters) { } private void addStatusAction(MenuManager menuManager) { - String copilotStatus = getCopilotStatusBasedOnAuthAndCompletionResult(copilotStatusManager.getCopilotStatus()); + String copilotStatus = getCopilotStatusBasedOnAuthAndCompletionResult(authStatusManager.getCopilotStatus()); String copilotStatusTitle = Messages.menu_copilotStatus + ": " + copilotStatus; MenuActionFactory.createMenuAction(menuManager, copilotStatusTitle, handlerService, copilotStatus, false); @@ -116,7 +116,7 @@ private void addLinkToFeedbackForumAction(MenuManager menuManager) { } private String getCopilotStatusBasedOnAuthAndCompletionResult(String copilotStatus) { - CompletionStatusManager completionStatusManager = getCompletionStatusManager(); + CopilotStatusManager completionStatusManager = getAuthAndCompletionStatusManager(); switch (copilotStatus) { case CopilotStatusResult.OK: return completionStatusManager.isCompletionInProgress() ? Messages.menu_copilotStatus_completionInProgress @@ -147,7 +147,7 @@ private void scheduleSpinnerJob(UIElement uiElement) { } private void addSignInOrSignOutAction(MenuManager menuManager) { - if (Objects.equals(copilotStatusManager.getCopilotStatus(), CopilotStatusResult.OK)) { + if (Objects.equals(authStatusManager.getCopilotStatus(), CopilotStatusResult.OK)) { ImageDescriptor signInIcon = UiUtils.buildImageDescriptorFromPngPath("/icons/signin.png"); MenuActionFactory.createMenuAction(menuManager, Messages.menu_signOutFromGitHub, signInIcon, handlerService, "com.microsoft.copilot.eclipse.commands.signOut", true); @@ -209,7 +209,7 @@ protected IStatus run(IProgressMonitor monitor) { ImageDescriptor newIcon = UiUtils.buildImageDescriptorFromPngPath(iconPath); this.uiElement.setIcon(newIcon); currentIconIndex = (currentIconIndex % TOTAL_SPINNER_ICONS) + 1; - if (CopilotUi.getPlugin().getCompletionStatusManager().isCompletionInProgress()) { + if (CopilotUi.getPlugin().getAuthAndCompletionStatusManager().isCompletionInProgress()) { schedule(COMPLETION_IN_PROGRESS_SPINNER_ROTATE_RATE_MILLIS); } else { cancel(); diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/SignInHandler.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/SignInHandler.java index d3c18e50..fdc35e98 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/SignInHandler.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/SignInHandler.java @@ -6,13 +6,18 @@ import org.eclipse.core.commands.AbstractHandler; import org.eclipse.core.commands.ExecutionEvent; import org.eclipse.core.commands.ExecutionException; +import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Status; +import org.eclipse.core.runtime.jobs.Job; import org.eclipse.jface.dialogs.MessageDialog; import org.eclipse.swt.widgets.Shell; +import com.microsoft.copilot.eclipse.core.AuthStatusManager; +import com.microsoft.copilot.eclipse.core.Constants; import com.microsoft.copilot.eclipse.core.CopilotCore; -import com.microsoft.copilot.eclipse.core.CopilotStatusManager; import com.microsoft.copilot.eclipse.core.logger.LogLevel; +import com.microsoft.copilot.eclipse.core.lsp.protocol.CopilotStatusResult; import com.microsoft.copilot.eclipse.core.lsp.protocol.SignInInitiateResult; import com.microsoft.copilot.eclipse.ui.CopilotUi; import com.microsoft.copilot.eclipse.ui.dialogs.SignInConfirmDialog; @@ -26,89 +31,115 @@ */ public class SignInHandler extends AbstractHandler { - private static final long SIGNIN_TIMEOUT_MILLIS = 180000L; - - private CopilotStatusManager copilotStatusManager; + private AuthStatusManager authStatusManager; /** * Initialize the Copilot Language Server for the SignInHandler. */ public SignInHandler() { - this.copilotStatusManager = CopilotCore.getPlugin().getCopilotStatusManager(); + this.authStatusManager = CopilotCore.getPlugin().getAuthStatusManager(); } @Override public Object execute(ExecutionEvent event) throws ExecutionException { - Shell shell = SwtUtils.getShellFromEvent(event); + SignInJob signInJob = new SignInJob(event); + signInJob.schedule(); + + return null; + } + + private class SignInJob extends Job { + + private static final long SIGNIN_TIMEOUT_MILLIS = 180000L; + + private final ExecutionEvent event; - try { + /** + * Creates a new completion job. + */ + public SignInJob(ExecutionEvent event) { + super("Initializing GitHub Copilot sign-in process..."); + this.event = event; + } + + @Override + protected IStatus run(IProgressMonitor monitor) { + try { + Shell shell = SwtUtils.getShellFromEvent(event); + IStatus status = runInitiateSignIn(shell); + return status; + } catch (Exception e) { + String msg = Messages.signInHandler_msgDialog_signInFailed; + if (StringUtils.isNotBlank(e.getMessage())) { + msg += " " + e.getMessage(); + CopilotUi.LOGGER.log(LogLevel.ERROR, msg, e); + } + + String errorMsg = "Sign in failed: " + e.getMessage(); + return new Status(IStatus.ERROR, Constants.PLUGIN_ID, errorMsg); + } + } + + private IStatus runInitiateSignIn(Shell shell) + throws InterruptedException, java.util.concurrent.ExecutionException { SignInInitiateResult result = initiateSignIn(); if (result.isAlreadySignedIn()) { showAlreadySignedInMessage(shell); + return Status.OK_STATUS; } else { handleSignIn(shell, result); + return Status.OK_STATUS; } - } catch (Exception e) { - handleSignInException(shell, e); } - return null; - } + private SignInInitiateResult initiateSignIn() throws InterruptedException, java.util.concurrent.ExecutionException { + return authStatusManager.signInInitiate(); + } - private SignInInitiateResult initiateSignIn() throws Exception { - return this.copilotStatusManager.signInInitiate(); - } + private void showAlreadySignedInMessage(Shell shell) { + MessageDialog.openInformation(shell, Messages.signInHandler_msgDialog_title, + Messages.signInHandler_msgDialog_alreadySignedIn); + } - private void showAlreadySignedInMessage(Shell shell) { - MessageDialog.openInformation(shell, Messages.signInHandler_msgDialog_title, - Messages.signInHandler_msgDialog_alreadySignedIn); - } + private void handleSignIn(Shell shell, SignInInitiateResult result) { + AtomicReference signInInitiateResultHolder = new AtomicReference<>(result); + SwtUtils.invokeOnDisplayThread(() -> { + SignInDialog signInDialog = new SignInDialog(shell, signInInitiateResultHolder.get()); + int openResult = signInDialog.open(); + if (openResult > 0) { + UiUtils.openLink(signInInitiateResultHolder.get().getVerificationUri()); + SignInConfirmDialog signInConfirmDialog = new SignInConfirmDialog(shell, + signInInitiateResultHolder.get().getUserCode(), SIGNIN_TIMEOUT_MILLIS); + signInConfirmDialog.run(); + handleSignInConfirmation(shell, signInConfirmDialog); + } + }); + } - private void handleSignIn(Shell shell, SignInInitiateResult result) { - AtomicReference signInInitiateResultHolder = new AtomicReference<>(result); - SwtUtils.invokeOnDisplayThread(() -> { - SignInDialog signInDialog = new SignInDialog(shell, signInInitiateResultHolder.get()); - int btnId = signInDialog.open(); - if (btnId > 0) { - UiUtils.openLink(signInInitiateResultHolder.get().getVerificationUri()); - SignInConfirmDialog signInConfirmDialog = new SignInConfirmDialog(shell, - signInInitiateResultHolder.get().getUserCode(), SIGNIN_TIMEOUT_MILLIS); - signInConfirmDialog.run(); - handleSignInConfirmation(shell, signInConfirmDialog); + private void handleSignInConfirmation(Shell shell, SignInConfirmDialog signInConfirmDialog) { + IStatus status = signInConfirmDialog.getStatus(); + if (status != null && status.isOK()) { + showSignInSuccessMessage(shell); + authStatusManager.setCopilotStatus(CopilotStatusResult.OK); + } else { + showSignInFailMessage(shell, status); + authStatusManager.setCopilotStatus(CopilotStatusResult.NOT_SIGNED_IN); } - }); - } - - private void handleSignInConfirmation(Shell shell, SignInConfirmDialog signInConfirmDialog) { - IStatus status = signInConfirmDialog.getStatus(); - if (status != null && status.isOK()) { - showSignInSuccessMessage(shell); - } else { - showSignInFailMessage(shell, status); } - } - - private void showSignInSuccessMessage(Shell shell) { - MessageDialog.openInformation(shell, Messages.signInHandler_msgDialog_githubCopilot, - Messages.signInHandler_msgDialog_signInSuccess); - } - private void showSignInFailMessage(Shell shell, IStatus status) { - String msg = Messages.signInHandler_msgDialog_signInFailed; - if (status != null && StringUtils.isNotBlank(status.getMessage())) { - msg += ": " + status.getMessage(); + private void showSignInSuccessMessage(Shell shell) { + MessageDialog.openInformation(shell, Messages.signInHandler_msgDialog_githubCopilot, + Messages.signInHandler_msgDialog_signInSuccess); } - msg += ". "; - MessageDialog.openInformation(shell, Messages.signInHandler_msgDialog_githubCopilot, - msg + Messages.signInHandler_msgDialog_signInFailedTryAgain); - } - private void handleSignInException(Shell shell, Exception e) { - String msg = Messages.signInHandler_msgDialog_signInFailed; - if (StringUtils.isNotBlank(e.getMessage())) { - msg += " " + e.getMessage(); - CopilotUi.LOGGER.log(LogLevel.ERROR, msg, e); + private void showSignInFailMessage(Shell shell, IStatus status) { + String msg = Messages.signInHandler_msgDialog_signInFailed; + if (status != null && StringUtils.isNotBlank(status.getMessage())) { + msg += ": " + status.getMessage(); + } + msg += ". "; + MessageDialog.openInformation(shell, Messages.signInHandler_msgDialog_githubCopilot, + msg + Messages.signInHandler_msgDialog_signInFailedTryAgain); } - MessageDialog.openError(shell, Messages.signInHandler_msgDialog_signInFailedFailure, msg); } } diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/SignOutHandler.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/SignOutHandler.java index 54f0b5b4..2c446363 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/SignOutHandler.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/SignOutHandler.java @@ -7,8 +7,8 @@ import org.eclipse.jface.dialogs.MessageDialog; import org.eclipse.swt.widgets.Shell; +import com.microsoft.copilot.eclipse.core.AuthStatusManager; import com.microsoft.copilot.eclipse.core.CopilotCore; -import com.microsoft.copilot.eclipse.core.CopilotStatusManager; import com.microsoft.copilot.eclipse.core.logger.LogLevel; import com.microsoft.copilot.eclipse.core.lsp.protocol.CopilotStatusResult; import com.microsoft.copilot.eclipse.ui.CopilotUi; @@ -20,23 +20,23 @@ */ public class SignOutHandler extends AbstractHandler { - private CopilotStatusManager copilotStatusManager; + private AuthStatusManager authStatusManager; /** * Initialize the Copilot Language Server and Auth Status Manager for the SignOutHandler. */ public SignOutHandler() { - this.copilotStatusManager = CopilotCore.getPlugin().getCopilotStatusManager(); + this.authStatusManager = CopilotCore.getPlugin().getAuthStatusManager(); } @Override public Object execute(ExecutionEvent event) throws ExecutionException { Shell shell = SwtUtils.getShellFromEvent(event); try { - CopilotStatusResult result = copilotStatusManager.signOut(); + CopilotStatusResult result = authStatusManager.signOut(); if (!result.isSignedIn()) { showSignOutMessage(shell); - copilotStatusManager.checkStatus(); + authStatusManager.checkStatus(); } } catch (Exception e) { handleSignOutException(shell, e); From 1b07a1efb54d43bc50c034863e3196d5fd00d813 Mon Sep 17 00:00:00 2001 From: yanshudan <1397370237@qq.com> Date: Thu, 2 Jan 2025 13:15:57 +0800 Subject: [PATCH 048/690] feat - add basic proxy and auth proxy (#47) --- .../META-INF/MANIFEST.MF | 3 +- .../LanguageServerSettingManagerTests.java | 95 ++++++ .../META-INF/MANIFEST.MF | 3 +- .../copilot/eclipse/core/Constants.java | 1 + .../copilot/eclipse/core/CopilotCore.java | 19 +- .../lsp/CopilotLanguageServerConnection.java | 10 +- .../lsp/LanguageServerSettingManager.java | 121 ++++++++ .../CopilotLanguageServerSettings.java | 284 ++++++++++++++++++ launch/plugin_debug_configuration.launch | 1 + 9 files changed, 532 insertions(+), 5 deletions(-) create mode 100644 com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/lsp/LanguageServerSettingManagerTests.java create mode 100644 com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/LanguageServerSettingManager.java create mode 100644 com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/CopilotLanguageServerSettings.java diff --git a/com.microsoft.copilot.eclipse.core.test/META-INF/MANIFEST.MF b/com.microsoft.copilot.eclipse.core.test/META-INF/MANIFEST.MF index e5428be1..aa98b007 100644 --- a/com.microsoft.copilot.eclipse.core.test/META-INF/MANIFEST.MF +++ b/com.microsoft.copilot.eclipse.core.test/META-INF/MANIFEST.MF @@ -15,4 +15,5 @@ Require-Bundle: com.microsoft.copilot.eclipse.core;bundle-version="0.1.0", org.mockito.junit-jupiter;bundle-version="5.14.2", org.eclipse.lsp4j, org.eclipse.core.jobs, - org.eclipse.equinox.common + org.eclipse.equinox.common, + org.eclipse.core.net;bundle-version="1.5.500" diff --git a/com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/lsp/LanguageServerSettingManagerTests.java b/com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/lsp/LanguageServerSettingManagerTests.java new file mode 100644 index 00000000..1cf97be7 --- /dev/null +++ b/com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/lsp/LanguageServerSettingManagerTests.java @@ -0,0 +1,95 @@ +package com.microsoft.copilot.eclipse.core.lsp; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.eclipse.core.net.proxy.IProxyData; +import org.eclipse.core.net.proxy.IProxyService; +import org.eclipse.lsp4j.DidChangeConfigurationParams; +import org.junit.jupiter.api.Test; + +import com.microsoft.copilot.eclipse.core.lsp.protocol.CopilotLanguageServerSettings; + +/** + * Tests for the LanguageServerSettingManager. + */ +public class LanguageServerSettingManagerTests { + @Test + void testNoProxy() { + // when no proxy is applicable + // arrange + IProxyService mockProxyService = mock(IProxyService.class); + CopilotLanguageServerConnection mockLsConnection = mock(CopilotLanguageServerConnection.class); + when(mockProxyService.select(any())).thenReturn(null); + var params = new DidChangeConfigurationParams(); + params.setSettings(new CopilotLanguageServerSettings()); + + // act + LanguageServerSettingManager manager = new LanguageServerSettingManager(mockLsConnection, mockProxyService); + manager.updateProxySettings(); + manager.syncConfiguration(); + + // assert + verify(mockLsConnection, times(1)).updateConfig(params); + } + + @Test + void testBasicProxy() { + // basic proxy test + // arrange + IProxyService mockProxyService = mock(IProxyService.class); + IProxyData mockProxyData = mock(IProxyData.class); + when(mockProxyData.getHost()).thenReturn("localhost"); + when(mockProxyData.getPort()).thenReturn(8080); + when(mockProxyData.getType()).thenReturn("HTTPS"); + when(mockProxyData.isRequiresAuthentication()).thenReturn(false); + when(mockProxyService.select(any())).thenReturn(new IProxyData[] { mockProxyData }); + when(mockProxyService.isProxiesEnabled()).thenReturn(true); + var params = new DidChangeConfigurationParams(); + var settings = new CopilotLanguageServerSettings(); + settings.getHttp().setProxy("HTTPS://localhost:8080"); + params.setSettings(settings); + CopilotLanguageServerConnection mockLsConnection = mock(CopilotLanguageServerConnection.class); + + // act + LanguageServerSettingManager manager = new LanguageServerSettingManager(mockLsConnection, mockProxyService); + manager.updateProxySettings(); + manager.syncConfiguration(); + + // assert + verify(mockLsConnection, times(1)).updateConfig(params); + } + + @Test + void testBasicAuthProxy() { + // basic auth proxy test + // arrange + IProxyService mockProxyService = mock(IProxyService.class); + IProxyData mockProxyData = mock(IProxyData.class); + when(mockProxyData.getHost()).thenReturn("localhost"); + when(mockProxyData.getPort()).thenReturn(8080); + when(mockProxyData.getType()).thenReturn("HTTPS"); + when(mockProxyData.isRequiresAuthentication()).thenReturn(true); + when(mockProxyData.getUserId()).thenReturn("user"); + when(mockProxyData.getPassword()).thenReturn("password"); + when(mockProxyService.select(any())).thenReturn(new IProxyData[] { mockProxyData }); + when(mockProxyService.isProxiesEnabled()).thenReturn(true); + var params = new DidChangeConfigurationParams(); + var settings = new CopilotLanguageServerSettings(); + settings.getHttp().setProxy("HTTPS://user:password@localhost:8080"); + params.setSettings(settings); + CopilotLanguageServerConnection mockLsConnection = mock(CopilotLanguageServerConnection.class); + + // act + LanguageServerSettingManager manager = new LanguageServerSettingManager(mockLsConnection, mockProxyService); + manager.updateProxySettings(); + manager.syncConfiguration(); + + // assert + verify(mockLsConnection, times(1)).updateConfig(params); + } + +} diff --git a/com.microsoft.copilot.eclipse.core/META-INF/MANIFEST.MF b/com.microsoft.copilot.eclipse.core/META-INF/MANIFEST.MF index d8fcecc4..d1622fae 100644 --- a/com.microsoft.copilot.eclipse.core/META-INF/MANIFEST.MF +++ b/com.microsoft.copilot.eclipse.core/META-INF/MANIFEST.MF @@ -22,4 +22,5 @@ Require-Bundle: org.eclipse.lsp4e;bundle-version="0.18.12", org.eclipse.jdt.annotation;bundle-version="2.3.0", org.eclipse.jface.text, com.google.gson;bundle-version="2.11.0", - org.eclipse.wildwebdeveloper.embedder.node;bundle-version="1.0.3";resolution:=optional + org.eclipse.wildwebdeveloper.embedder.node;bundle-version="1.0.3";resolution:=optional, + org.eclipse.core.net;bundle-version="1.5.500" diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/Constants.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/Constants.java index 95d5479c..e0b7e0c7 100644 --- a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/Constants.java +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/Constants.java @@ -11,4 +11,5 @@ private Constants() { public static final String PLUGIN_ID = "com.microsoft.copilot.eclipse"; public static final String AUTO_SHOW_COMPLETION = "enableAutoCompletions"; + public static final String GITHUB_COPILOT_URL = "http://github.com"; } diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/CopilotCore.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/CopilotCore.java index cad2f071..2bf26a9f 100644 --- a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/CopilotCore.java +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/CopilotCore.java @@ -2,6 +2,7 @@ import java.util.Objects; +import org.eclipse.core.net.proxy.IProxyService; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Plugin; @@ -11,11 +12,15 @@ import org.eclipse.lsp4e.LanguageServersRegistry; import org.eclipse.lsp4e.LanguageServiceAccessor; import org.osgi.framework.BundleContext; +import org.osgi.framework.FrameworkUtil; +import org.osgi.framework.ServiceReference; import com.microsoft.copilot.eclipse.core.completion.CompletionProvider; import com.microsoft.copilot.eclipse.core.logger.CopilotForEclipseLogger; import com.microsoft.copilot.eclipse.core.logger.LogLevel; +import com.microsoft.copilot.eclipse.core.lsp.CopilotLanguageServer; import com.microsoft.copilot.eclipse.core.lsp.CopilotLanguageServerConnection; +import com.microsoft.copilot.eclipse.core.lsp.LanguageServerSettingManager; /** * The plug-in runtime class for the Copilot plug-in containing the core (UI-free) support, like the completion, @@ -26,6 +31,7 @@ public class CopilotCore extends Plugin { private CopilotLanguageServerConnection copilotLanguageServer; private AuthStatusManager authStatusManager; private CompletionProvider completionProvider; + private LanguageServerSettingManager languageServerSettingManager; private static CopilotCore COPILOT_CORE_PLUGIN = null; public static final CopilotForEclipseLogger LOGGER = new CopilotForEclipseLogger(CopilotCore.class.getName()); @@ -50,7 +56,7 @@ public static CopilotCore getPlugin() { @Override public void start(BundleContext context) throws Exception { - init(); + init(context); } @Override @@ -58,10 +64,13 @@ public void stop(BundleContext context) throws Exception { if (copilotLanguageServer != null) { copilotLanguageServer.stop(); } + if (this.languageServerSettingManager != null) { + this.languageServerSettingManager.dispose(); + } } @SuppressWarnings("restriction") - void init() { + void init(BundleContext context) { final Runnable initRunnable = () -> { LanguageServersRegistry.LanguageServerDefinition serverDef = LanguageServersRegistry.getInstance() .getDefinition(CopilotLanguageServerConnection.SERVER_ID); @@ -78,6 +87,12 @@ void init() { this.completionProvider = new CompletionProvider(this.copilotLanguageServer, authStatusManager); this.authStatusManager.checkStatus(); + // initialize the LanguageServerSettingManager + ServiceReference serviceReference = context.getServiceReference(IProxyService.class.getName()); + this.languageServerSettingManager = new LanguageServerSettingManager(this.copilotLanguageServer, + (IProxyService) context.getService(serviceReference)); + this.languageServerSettingManager.updateProxySettings(); + this.languageServerSettingManager.syncConfiguration(); }; Job initJob = new Job("GitHub Copilot Initialization...") { diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/CopilotLanguageServerConnection.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/CopilotLanguageServerConnection.java index 2b7078d1..8e2b768f 100644 --- a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/CopilotLanguageServerConnection.java +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/CopilotLanguageServerConnection.java @@ -7,6 +7,7 @@ import org.eclipse.jface.text.IDocument; import org.eclipse.lsp4e.LanguageServerWrapper; +import org.eclipse.lsp4j.DidChangeConfigurationParams; import org.eclipse.lsp4j.services.LanguageServer; import com.microsoft.copilot.eclipse.core.AuthStatusManager; @@ -84,7 +85,14 @@ public CompletableFuture getCompletions(CompletionParams param } /** - * Please use the {@link AuthStatusManager#signInInitiate()} method instead. + * Update the configuration for the language server. + */ + public void updateConfig(DidChangeConfigurationParams params) { + this.languageServerWrapper.sendNotification(server -> server.getWorkspaceService().didChangeConfiguration(params)); + } + + /** + * Please use the {@link CopilotStatusManager#signInInitiate()} method instead. *

* Initiate the sign in process. */ diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/LanguageServerSettingManager.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/LanguageServerSettingManager.java new file mode 100644 index 00000000..be99ef22 --- /dev/null +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/LanguageServerSettingManager.java @@ -0,0 +1,121 @@ +package com.microsoft.copilot.eclipse.core.lsp; + +import java.net.URI; + +import org.eclipse.core.net.proxy.IProxyChangeEvent; +import org.eclipse.core.net.proxy.IProxyChangeListener; +import org.eclipse.core.net.proxy.IProxyData; +import org.eclipse.core.net.proxy.IProxyService; +import org.eclipse.lsp4j.DidChangeConfigurationParams; + +import com.microsoft.copilot.eclipse.core.Constants; +import com.microsoft.copilot.eclipse.core.CopilotCore; +import com.microsoft.copilot.eclipse.core.logger.LogLevel; +import com.microsoft.copilot.eclipse.core.lsp.protocol.CopilotLanguageServerSettings; + +/** + * A class to manage the proxy service for the Copilot Language Server. + */ +public class LanguageServerSettingManager implements IProxyChangeListener { + IProxyService proxyService = null; + CopilotLanguageServerSettings settings = new CopilotLanguageServerSettings(); + CopilotLanguageServerConnection copilotLanguageServerConnection = null; + + /** + * Initializes the LanguageServerSettingManager. + */ + public LanguageServerSettingManager(CopilotLanguageServerConnection conn, IProxyService proxyService) { + this.copilotLanguageServerConnection = conn; + this.proxyService = proxyService; + + // add listners + proxyService.addProxyChangeListener(this); + } + + /** + * A listener for the proxy service. + */ + @Override + public void proxyInfoChanged(IProxyChangeEvent event) { + CopilotCore.LOGGER.log(LogLevel.INFO, "Proxy info changed"); + updateProxySettings(); + syncConfiguration(); + } + + /** + * Synchronizes the configuration with the language server. + */ + public void syncConfiguration() { + DidChangeConfigurationParams params = new DidChangeConfigurationParams(); + params.setSettings(settings); + this.copilotLanguageServerConnection.updateConfig(params); + } + + /** + * Updates the proxy settings. + */ + public void updateProxySettings() { + IProxyData proxyData = getProxy(); + if (proxyData == null) { + settings.getHttp().setProxy(null); + CopilotCore.LOGGER.log(LogLevel.INFO, "No proxy data found"); + return; + } + settings.getHttp().setProxy(createProxyString(proxyData)); + CopilotCore.LOGGER.log(LogLevel.INFO, String.format("Proxy will be updated to %s", settings.getHttp().getProxy())); + + } + + /** + * Gets the proxy data. + * + * @return the proxy data + */ + private IProxyData getProxy() { + if (proxyService == null) { + CopilotCore.LOGGER.log(LogLevel.ERROR, "Proxy service is null"); + return null; + } + if (!proxyService.isProxiesEnabled()) { + CopilotCore.LOGGER.log(LogLevel.INFO, "Proxies are disabled"); + return null; + } + IProxyData[] proxyData = proxyService.select(URI.create(Constants.GITHUB_COPILOT_URL)); + if (proxyData != null && proxyData.length > 0) { + return proxyData[0]; + } + return null; + } + + /** + * Creates a proxy string from the given proxy data. + * + * @param proxyData the proxy data + * @return the proxy string + */ + public static String createProxyString(IProxyData proxyData) { + if (proxyData == null) { + CopilotCore.LOGGER.log(LogLevel.ERROR, "Proxy data is null"); + return null; + } + + String proxyString = proxyData.getType() + "://"; + String host = proxyData.getHost(); + int port = proxyData.getPort(); + String user = proxyData.getUserId(); + String password = proxyData.getPassword(); + + if (proxyData.isRequiresAuthentication()) { + proxyString += user + ":" + password + "@"; + } + proxyString += host + ":" + port; + return proxyString; + } + + /** + * Disposes the resources of this LanguageServerSettingManager. + */ + public void dispose() { + proxyService.removeProxyChangeListener(this); + } +} diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/CopilotLanguageServerSettings.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/CopilotLanguageServerSettings.java new file mode 100644 index 00000000..2e84f54c --- /dev/null +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/CopilotLanguageServerSettings.java @@ -0,0 +1,284 @@ +package com.microsoft.copilot.eclipse.core.lsp.protocol; + +import java.util.Objects; + +import com.google.gson.annotations.SerializedName; +import org.eclipse.lsp4j.jsonrpc.util.ToStringBuilder; + +/** + * Settings for the DidChangeConfigurationParams. + */ +public class CopilotLanguageServerSettings { + private boolean showEditorCompletions; + private boolean enableAutoCompletions; + + /** + * Http settings. + */ + public class Http { + + private String proxy; + @SerializedName("proxyStrictSSL") + private boolean proxyStrictSsl; + private String proxyKerberosServicePrincipal; + + + /** + * get proxy. + * + * @return the proxy + */ + public String getProxy() { + return proxy; + } + + /** + * set proxy. + * + * @param proxy the proxy to set + */ + public void setProxy(String proxy) { + this.proxy = proxy; + } + + /** + * is proxy strict ssl. + * + * @return the proxyStrictSsl + */ + public boolean isProxyStrictSsl() { + return proxyStrictSsl; + } + + /** + * set proxy strict ssl. + * + * @param proxyStrictSsl the proxyStrictSsl to set + */ + public void setProxyStrictSsl(boolean proxyStrictSsl) { + this.proxyStrictSsl = proxyStrictSsl; + } + + /** + * get proxy kerberos service principal. + * + * @return the proxyKerberosServicePrincipal + */ + public String getProxyKerberosServicePrincipal() { + return proxyKerberosServicePrincipal; + } + + /** + * set proxy kerberos service principal. + * + * @param proxyKerberosServicePrincipal the proxyKerberosServicePrincipal to set + */ + public void setProxyKerberosServicePrincipal(String proxyKerberosServicePrincipal) { + this.proxyKerberosServicePrincipal = proxyKerberosServicePrincipal; + } + + @Override + public String toString() { + ToStringBuilder builder = new ToStringBuilder(this); + builder.add("proxy", proxy); + builder.add("proxyStrictSsl", proxyStrictSsl); + builder.add("proxyKerberosServicePrincipal", proxyKerberosServicePrincipal); + return builder.toString(); + } + + @Override + public int hashCode() { + return Objects.hash(proxy, proxyKerberosServicePrincipal, proxyStrictSsl); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + Http other = (Http) obj; + return Objects.equals(proxy, other.proxy) + && Objects.equals(proxyKerberosServicePrincipal, other.proxyKerberosServicePrincipal) + && proxyStrictSsl == other.proxyStrictSsl; + } + + } + + /** + * Github Enterprise settings. + */ + public class GithubEnterprise { + private String uri = "http://github.com"; + + /** + * get Uri. + * + * @return the uri + */ + public String getUri() { + return uri; + } + + /** + * set Uri. + * + * @param uri the uri to set + */ + public void setUri(String uri) { + this.uri = uri; + } + + @Override + public String toString() { + ToStringBuilder builder = new ToStringBuilder(this); + builder.add("uri", uri); + return builder.toString(); + } + + @Override + public int hashCode() { + return Objects.hash(uri); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + GithubEnterprise other = (GithubEnterprise) obj; + return Objects.equals(uri, other.uri); + } + + } + + @SerializedName("github-enterprise") + private GithubEnterprise githubEnterprise; + private Http http; + + /** + * Constructor. + */ + public CopilotLanguageServerSettings() { + this.showEditorCompletions = true; + this.enableAutoCompletions = true; + this.http = new Http(); + this.githubEnterprise = new GithubEnterprise(); + } + + /** + * is show editor completions. + * + * @return the showEditorCompletions + */ + public boolean isShowEditorCompletions() { + return showEditorCompletions; + } + + /** + * set show editor completions. + * + * @param showEditorCompletions the showEditorCompletions to set + */ + public void setShowEditorCompletions(boolean showEditorCompletions) { + this.showEditorCompletions = showEditorCompletions; + } + + /** + * is enable auto completions. + * + * @return the enableAutoCompletions + */ + public boolean isEnableAutoCompletions() { + return enableAutoCompletions; + } + + /** + * set enable auto completions. + * + * @param enableAutoCompletions the enableAutoCompletions to set + */ + public void setEnableAutoCompletions(boolean enableAutoCompletions) { + this.enableAutoCompletions = enableAutoCompletions; + } + + /** + * get github enterprise. + * + * @return the githubEnterprise + */ + public GithubEnterprise getGithubEnterprise() { + return githubEnterprise; + } + + /** + * set github enterprise. + * + * @param githubEnterprise the githubEnterprise to set + */ + public void setGithubEnterprise(GithubEnterprise githubEnterprise) { + this.githubEnterprise = githubEnterprise; + } + + /** + * get http. + * + * @return the http + */ + public Http getHttp() { + return http; + } + + /** + * set http. + * + * @param http the http to set + */ + public void setHttp(Http http) { + this.http = http; + } + + @Override + public String toString() { + ToStringBuilder builder = new ToStringBuilder(this); + builder.add("showEditorCompletions", showEditorCompletions); + builder.add("enableAutoCompletions", enableAutoCompletions); + builder.add("githubEnterprise", githubEnterprise); + builder.add("http", http); + return builder.toString(); + } + + @Override + public int hashCode() { + return Objects.hash(enableAutoCompletions, githubEnterprise, http, showEditorCompletions); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + CopilotLanguageServerSettings other = (CopilotLanguageServerSettings) obj; + return enableAutoCompletions == other.enableAutoCompletions + && Objects.equals(githubEnterprise, other.githubEnterprise) && Objects.equals(http, other.http) + && showEditorCompletions == other.showEditorCompletions; + } + +} diff --git a/launch/plugin_debug_configuration.launch b/launch/plugin_debug_configuration.launch index f937ebb3..0eb4d706 100644 --- a/launch/plugin_debug_configuration.launch +++ b/launch/plugin_debug_configuration.launch @@ -84,6 +84,7 @@ + From 5a3e7e7e45ad8d35d4b13aa6fec7d4cbbffdb550 Mon Sep 17 00:00:00 2001 From: ethanyhou <149548697+ethanyhou@users.noreply.github.com> Date: Fri, 3 Jan 2025 16:12:44 +0800 Subject: [PATCH 049/690] [feat] - Enabled key bindings on the preferences settings, updated context menu, and updated project name to GitHub Copilot. (#87) --- com.microsoft.copilot.eclipse.core/plugin.xml | 2 +- .../core/lsp/LsStreamConnectionProvider.java | 2 +- .../feature.xml | 2 +- .../category.xml | 2 +- .../icons/edit_keyboard_shortcuts.png | Bin 0 -> 604 bytes .../icons/edit_keyboard_shortcuts@2x.png | Bin 0 -> 1266 bytes .../icons/edit_preferences.png | Bin 0 -> 732 bytes .../icons/edit_preferences@2x.png | Bin 0 -> 1512 bytes .../plugin.properties | 7 ++-- com.microsoft.copilot.eclipse.ui/plugin.xml | 31 +++++++++++++++++- .../OpenEditKeyboardShortcutsHandler.java | 31 ++++++++++++++++++ .../ui/handlers/OpenPreferencesHandler.java | 28 ++++++++++++++++ .../ui/handlers/ShowStatusBarMenuHandler.java | 21 ++++++++++-- .../copilot/eclipse/ui/i18n/Messages.java | 2 ++ .../eclipse/ui/i18n/messages.properties | 4 ++- pom.xml | 2 +- 16 files changed, 123 insertions(+), 11 deletions(-) create mode 100644 com.microsoft.copilot.eclipse.ui/icons/edit_keyboard_shortcuts.png create mode 100644 com.microsoft.copilot.eclipse.ui/icons/edit_keyboard_shortcuts@2x.png create mode 100644 com.microsoft.copilot.eclipse.ui/icons/edit_preferences.png create mode 100644 com.microsoft.copilot.eclipse.ui/icons/edit_preferences@2x.png create mode 100644 com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/OpenEditKeyboardShortcutsHandler.java create mode 100644 com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/OpenPreferencesHandler.java diff --git a/com.microsoft.copilot.eclipse.core/plugin.xml b/com.microsoft.copilot.eclipse.core/plugin.xml index a55a9fda..8954f14a 100644 --- a/com.microsoft.copilot.eclipse.core/plugin.xml +++ b/com.microsoft.copilot.eclipse.core/plugin.xml @@ -12,7 +12,7 @@ point="org.eclipse.lsp4e.languageServer"> - + \ No newline at end of file diff --git a/com.microsoft.copilot.eclipse.ui/icons/edit_keyboard_shortcuts.png b/com.microsoft.copilot.eclipse.ui/icons/edit_keyboard_shortcuts.png new file mode 100644 index 0000000000000000000000000000000000000000..70a850f05c4e070e8d0bc455ed0cd2796918dc3e GIT binary patch literal 604 zcmV-i0;BzjP)ypzjN++{Lb&3101xFdam4iGhZleCW0W~ zZJ!ivO94x(1X}2xOq+>nMZ(zv~&1d0>@)NE8j-=HwUt_35>NrE&)t_MmMaStgbx zU%lN#n!&M{Q$`JC7iItWKdEf`{C8aYKKvZDF;ZH8K`Cb#yzTB2?Jpmb8kVX^mdjaq zhfR4cDnUIa2ldw*NiVU+iBQOPSD}#khN9g$-ET?3^Rnb+bMXDm^7U{y0doa8LKvz} ztOLD6Y3w>D+*lOp!7l8T(+$rJK#d!PuszlJo<6vyhGG54h0M;)VQFO@Ey-_KTwRBk z$z$kGhXyeKvFJj2dNz?SmZ(tjAvFp`p9)gvi#}pb6m@57(cbzHFOuJNVcnTpR4lJj zy}Y=*#*I(jbN}!JU%1hxnDVLmXQH|?2*RV*E_5cFapz_OE?qpQy^*6wh$<<+)s*6R q+{MWg$FY&g%8TT*-taPv{mK?O==G6UMXtF30000Q6lD5$LgT)6?s5Cy6V0|cJpbaESpft2Kg_8c-AKDi~ zTViPi1@j^Zf<8qwA`u@1K@dezM11fM$-TRKjx%$9_UvAxBKJ)PmYp*@=ggdMzB6Y5 zp30Lh=#)3sZyjo;+4eNc7Avhm(}Rfq&7yhGI+tz8-=Zi&>*LoA<~ox`Sr{(R*Rj}H zyI)&A6CplBYlt)ZR~NIodvu@!nn*fKt}0~L8b7YS9;cc%Fc^?0-Z@6(VdhFBobQHLmswNeW0<^_r0|2rZp4{cl~_Fw*;mOXCUv{Dux-+4&pj}EMs zP+$FaK*rw=uR{*@<*x_Oj(u`Gjv=S72q6LxM>#5O_QBZ(&;r{-OqI4s_SKXv@OHr4r;?2OOx;DM06B69BKf z#hky;Nd;qN0{O8vJb@s%1~)bc6m5#+^B;HVEX(1G!Hp0xq+rjzL2x82l37S4w(ZOWvg1RcbI zEJNQst)e^x24>qxgb!v_Ar*f3$rv=6Dakz( zltRIzUYF#LO??NCA}{uCl6T}0p#&HO>N~&>~msv)fJtF~c-nk1mDX0rPvZW732KszC zS`tvo5ysv|?qwK((g+O?j8*tZ2v7-J0v~;_0`~5ohSO&+wxH0x2UBSfAW6T=JKHNobR--~596K?u0qM8?4^2`dlLwy=@4*-t#hCzIqLozw;*4>C|vgZSDs7(UqRA?8E;G c#tu*A9#%Zn@b=Iz761SM07*qoM6N<$f*;REwg3PC literal 0 HcmV?d00001 diff --git a/com.microsoft.copilot.eclipse.ui/icons/edit_preferences.png b/com.microsoft.copilot.eclipse.ui/icons/edit_preferences.png new file mode 100644 index 0000000000000000000000000000000000000000..2c6abc592ea45390a1fcaf4d2d2a2b003540b475 GIT binary patch literal 732 zcmV<20wev2P)?5xNSR2uT|uqA23lU~MbZCSDZ$SwRGA!PKjV)`Q|P-U_|co3|_&rBoqR zL`)ExOIs<5D}ohN3`wd<)Mhm%o84{p`QDm@1}Pq-1Ixa*^L;b(&CCM)r=j&6*G4YR z0^q^=_2ES1p775AH$ow=!RVvy279xN^jK*0*dQPT;^ecW1G8r(y~lOf@*n zT5=6(vt7RJV`cItpE%QjG0no2O`lVs7>aH1ER93o!Mzc$Z(HxBD@O?dyD)k9L!H2J zcF3f@xw;Q~UBXZgPJJkBDHcm;UV8JiB|y&sf4o%ubsQPGYar+Y-tPvxmBALKhIqdR zx^{Aqj4z?rFVd0&S4NW3yJ}Oy^~*ysmS`sR!l(BIz`BCvLJ_9LMRe!(0?7Flz^Vc% z;-hUVBS-8{lL0o>WeUT?d(299xHe;~R?wpdZ^5(6ZxRxZB{+9}H$HXJi)6XVQ(QE< z1kC`Zt?DE{K>24GvA{x`6*Y%ct!QAV3Tk6&1h)!ntsVd+qf;O0f$MbJ`3r&vf=9OE zCy51QvoUZY=mbMi;QmYo(pg0!IGMzjTlOFv3Ynwo@9yCH{T!VHDF`826+gN;D; zt3E^K9H2VMG66OWs#Ml78z5EPq8N&8@YesC$Zin6&}i~RP)=ie0ttz{*<=J*3k6bfK84-ywRu4+4t?VHhm!ft zZ7+dmZ52n2)YILMhrc?Xvaj9e^I*}Wd++Cm4(GC;GXuM}w|*3h>76m0>S_12dQzoi z{1TzLPIe9$?Iaz)6C3bv0LVrn*b{#(<8J9p0l|?!*2XmF464ik2zzpYu;g=Yq|q3kheoZ; zt`u4A8W&W|FJiAAhm{M4-v!Sa2T6NxZdnCZYcbpz8U^pBrQoiv0E%fOnW$Oz6l{HeJ)Jrc z#PYc%(Dcq~By9?$PN8de7_`uIe;Mb1sx_iYW$kJQG;di&r0ba2LCUuIDh0q~ zgJyFa{U!i`OM}WeYc=lk9}Gag5(u3OTpe!ktmt4TEqCvXS;rnkux4omOLUG{N77?} zy3R}Wa62*vilLOlcnCU<^};XR138_il%Rb_+fLq`dejJHlXv7ngzZPo02K|~8iBsw z!YW)Y0Xi3#lZsv)OOSpsjfA+>ck%>HwtRZufrI`O0HEs`MyQd@^&<`3vmsWMMppuXUJ#S`Znn_f%o`|;>;=nIC?xyVL3mqCYJJlM&V@F$ z+wt@)YN)HgvlqfD?ItzU&4Wb?tVEh-^vcj*@a>TS$VZO>5NzY)vLQukbg!qY`yRAx zUSx8y{)KXIESN#pZ${L6b=7Q;+EbL%AJ0@*cQ_s{p5pjJDQ0!LoG=DiDEU@fz1$8j z*FTvgO`dnDr0!<^Io=t@-fQE+;z0yi2T@Tz=gX=~;FX4QlIh2T5J8U-#B^io$y)J9 zH6X+%qx`LQ&cd-jV-S8AwKAXV>-DZgcGDh<4I}N@Mh?W&ZS`%uZxiS7R;h6tKqN_VD zj6?9|gwnY^JPA$f?CfBM8V@&qpM+ySKT61W_`s&MrK(Vlied=%D^VGO#m#>MIWr*l zb-ibWoLFx^8@6-v_w@Y@!CRA{E*8LUUbtPAMKrI}qDnCwjza$*3Md;#t3>>0T!V=z z9Zp_QeIR4x<9+_G_fPki@r&qF|G_=7LDnZPMj#lP0`pem+^VxhF+%Ih>FI zu#V^kZ%wiDNW!_;%;YQUH(@e}Ojm0B_z+Ax4oW8uBA@e|089`%S>DKTp9>D0KH)^S zh+->=ERDIGn+6J^UE8+!x$(AUvF7J{ej5}(1^;KH69lpg;WYeD1=2rdw7K|4< + + + + + + @@ -102,6 +123,14 @@ class="com.microsoft.copilot.eclipse.ui.handlers.ViewFeedbackForumHandler" commandId="com.microsoft.copilot.eclipse.commands.viewFeedbackForum"> + + + + @@ -126,4 +155,4 @@ sequence="ESC"> - \ No newline at end of file + diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/OpenEditKeyboardShortcutsHandler.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/OpenEditKeyboardShortcutsHandler.java new file mode 100644 index 00000000..0b2db97b --- /dev/null +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/OpenEditKeyboardShortcutsHandler.java @@ -0,0 +1,31 @@ +package com.microsoft.copilot.eclipse.ui.handlers; + +import org.eclipse.core.commands.AbstractHandler; +import org.eclipse.core.commands.ExecutionEvent; +import org.eclipse.core.commands.ExecutionException; +import org.eclipse.jface.preference.IPreferenceNode; +import org.eclipse.jface.preference.PreferenceDialog; +import org.eclipse.jface.preference.PreferenceManager; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.dialogs.PreferencesUtil; + +import com.microsoft.copilot.eclipse.ui.utils.SwtUtils; + +/** + * Handler for opening the preferences dialog. + */ +public class OpenEditKeyboardShortcutsHandler extends AbstractHandler { + + @Override + public Object execute(ExecutionEvent event) throws ExecutionException { + Shell shell = SwtUtils.getShellFromEvent(event); + PreferenceDialog dialog = PreferencesUtil.createPreferenceDialogOn(shell, + "org.eclipse.ui.preferencePages.Keys", + new String[] { "org.eclipse.ui.preferencePages.Keys" }, null); + dialog.open(); + + return null; + } + +} diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/OpenPreferencesHandler.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/OpenPreferencesHandler.java new file mode 100644 index 00000000..5a1f01a4 --- /dev/null +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/OpenPreferencesHandler.java @@ -0,0 +1,28 @@ +package com.microsoft.copilot.eclipse.ui.handlers; + +import org.eclipse.core.commands.AbstractHandler; +import org.eclipse.core.commands.ExecutionEvent; +import org.eclipse.core.commands.ExecutionException; +import org.eclipse.jface.preference.PreferenceDialog; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.ui.dialogs.PreferencesUtil; + +import com.microsoft.copilot.eclipse.ui.utils.SwtUtils; + +/** + * Handler for opening the preferences dialog. + */ +public class OpenPreferencesHandler extends AbstractHandler { + + @Override + public Object execute(ExecutionEvent event) throws ExecutionException { + Shell shell = SwtUtils.getShellFromEvent(event); + PreferenceDialog dialog = PreferencesUtil.createPreferenceDialogOn(shell, + "com.microsoft.copilot.eclipse.ui.preferences.page", + new String[] { "com.microsoft.copilot.eclipse.ui.preferences.page" }, null); + dialog.open(); + + return null; + } + +} diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/ShowStatusBarMenuHandler.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/ShowStatusBarMenuHandler.java index 43e0dcf6..ceaa4a5d 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/ShowStatusBarMenuHandler.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/ShowStatusBarMenuHandler.java @@ -58,6 +58,11 @@ public Object execute(ExecutionEvent event) throws ExecutionException { addSignInOrSignOutAction(menuManager); } + // Preferences section + menuManager.add(new Separator()); + addEditKeyboardShortcutsAction(menuManager); + addPreferencesAction(menuManager); + Shell shell = PlatformUI.getWorkbench().getDisplay().getActiveShell(); Menu menu = menuManager.createContextMenu(shell); menu.setVisible(true); @@ -115,6 +120,19 @@ private void addLinkToFeedbackForumAction(MenuManager menuManager) { "com.microsoft.copilot.eclipse.commands.viewFeedbackForum", true); } + private void addPreferencesAction(MenuManager menuManager) { + ImageDescriptor editPreferencesIcon = UiUtils.buildImageDescriptorFromPngPath("/icons/edit_preferences.png"); + MenuActionFactory.createMenuAction(menuManager, Messages.menu_editPreferences, editPreferencesIcon, handlerService, + "com.microsoft.copilot.eclipse.commands.openPreferences", true); + } + + private void addEditKeyboardShortcutsAction(MenuManager menuManager) { + ImageDescriptor editKeyboardShortcutsIcon = UiUtils + .buildImageDescriptorFromPngPath("/icons/edit_keyboard_shortcuts.png"); + MenuActionFactory.createMenuAction(menuManager, Messages.menu_editKeyboardShortcuts, editKeyboardShortcutsIcon, + handlerService, "com.microsoft.copilot.eclipse.commands.openEditKeyboardShortcuts", true); + } + private String getCopilotStatusBasedOnAuthAndCompletionResult(String copilotStatus) { CopilotStatusManager completionStatusManager = getAuthAndCompletionStatusManager(); switch (copilotStatus) { @@ -185,7 +203,6 @@ private class SpinnerJob extends Job { private static final int INITIAL_ICON_INDEX = 1; private static final int TOTAL_SPINNER_ICONS = 8; private static final long COMPLETION_IN_PROGRESS_SPINNER_ROTATE_RATE_MILLIS = 200L; - private int currentIconIndex = INITIAL_ICON_INDEX; private UIElement uiElement; @@ -194,7 +211,7 @@ public SpinnerJob() { super("Spinner Job"); this.setSystem(true); } - + public void setTargetUiElement(UIElement uiElement) { this.uiElement = uiElement; } diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/i18n/Messages.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/i18n/Messages.java index 62924551..85a17a9a 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/i18n/Messages.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/i18n/Messages.java @@ -18,6 +18,8 @@ public final class Messages extends NLS { public static String menu_signToGitHub; public static String menu_signOutFromGitHub; public static String menu_viewFeedbackForum; + public static String menu_editPreferences; + public static String menu_editKeyboardShortcuts; public static String signInDialog_title; public static String signInDialog_button_cancel; public static String signInDialog_button_copyOpen; diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/i18n/messages.properties b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/i18n/messages.properties index 99559d83..8eb4c8e7 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/i18n/messages.properties +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/i18n/messages.properties @@ -9,6 +9,8 @@ menu_copilotStatus_agentWarning=Copilot is encountering temporary issues menu_signToGitHub=Sign In to GitHub menu_signOutFromGitHub=Sign Out from GitHub menu_viewFeedbackForum=View Feedback Forum... +menu_editPreferences=Edit Preferences... +menu_editKeyboardShortcuts=Edit Keyboard Shortcuts... signInDialog_title=Sign In to GitHub signInDialog_button_cancel=Cancel @@ -33,5 +35,5 @@ signOutHandler_msgDialog_signOutSuccess=You have successfully signed out from Co signOutHandler_msgDialog_signOutFailed=Unable to sign out to GitHub Copilot at this time signOutHandler_msgDialog_signOutFailedFailure=Copilot Sign Out Failure -preferencesPage_description=Configure GitHub Copilot for Eclipse +preferencesPage_description=Configure GitHub Copilot preferencesPage_autoShowCompletion=Automatically show inline completions \ No newline at end of file diff --git a/pom.xml b/pom.xml index aa586a87..066952f4 100644 --- a/pom.xml +++ b/pom.xml @@ -11,7 +11,7 @@ ${base.name} - GitHub Copilot for Eclipse + GitHub Copilot 4.0.10 3.6.0 From 1402f145a74d1ca5dc2553c9140b987f0e05edca Mon Sep 17 00:00:00 2001 From: yanshudan <1397370237@qq.com> Date: Mon, 6 Jan 2025 08:57:41 +0800 Subject: [PATCH 050/690] fix - Remove duplicated loggers (#88) --- .../core/logger/CopilotForEclipseLogger.java | 2 +- .../com/microsoft/copilot/eclipse/ui/CopilotUi.java | 5 ++--- .../eclipse/ui/completion/CompletionHandler.java | 13 +++++++------ .../eclipse/ui/completion/CompletionManager.java | 5 +++-- .../eclipse/ui/dialogs/SignInConfirmDialog.java | 4 ++-- .../ui/handlers/ShowStatusBarMenuHandler.java | 2 +- .../copilot/eclipse/ui/handlers/SignInHandler.java | 4 ++-- .../copilot/eclipse/ui/handlers/SignOutHandler.java | 2 +- .../ui/handlers/ViewFeedbackForumHandler.java | 3 ++- .../microsoft/copilot/eclipse/ui/utils/UiUtils.java | 3 ++- 10 files changed, 23 insertions(+), 20 deletions(-) diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/logger/CopilotForEclipseLogger.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/logger/CopilotForEclipseLogger.java index 44ef6738..fb8bfbaa 100644 --- a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/logger/CopilotForEclipseLogger.java +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/logger/CopilotForEclipseLogger.java @@ -14,7 +14,7 @@ * The logger for Copilot for Eclipse. */ public class CopilotForEclipseLogger { - //TODO: migrate to xml configuration + // TODO: migrate to xml configuration private Logger logger; /** diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/CopilotUi.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/CopilotUi.java index 84dbfd49..bcaa6aa0 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/CopilotUi.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/CopilotUi.java @@ -29,7 +29,6 @@ public class CopilotUi extends AbstractUIPlugin { private CopilotStatusManager copilotStatusManager; private EditorLifecycleListener editorLifecycleListener; private EditorsManager editorsManager; - public static final CopilotForEclipseLogger LOGGER = new CopilotForEclipseLogger(CopilotCore.class.getName()); /** * Creates the Copilot ui plugin. The plugin is created automatically by the Eclipse framework. Clients must not call @@ -56,7 +55,7 @@ protected IStatus run(IProgressMonitor monitor) { CopilotLanguageServerConnection connection = CopilotCore.getPlugin().getCopilotLanguageServer(); if (connection == null) { var ex = new IllegalStateException("Failed to start copilot language server."); - LOGGER.log(LogLevel.ERROR, ex); + CopilotCore.LOGGER.log(LogLevel.ERROR, ex); throw ex; } @@ -73,7 +72,7 @@ protected IStatus run(IProgressMonitor monitor) { // to initialize it. initCompletionHandlerForActiveEditor(); } catch (OperationCanceledException | InterruptedException e) { - LOGGER.log(LogLevel.ERROR, e); + CopilotCore.LOGGER.log(LogLevel.ERROR, e); return Status.error("Failed to initialize GitHub Copilot plugin.", e); } return Status.OK_STATUS; diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionHandler.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionHandler.java index e2bfc4e8..e7056b38 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionHandler.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionHandler.java @@ -19,6 +19,7 @@ import org.eclipse.ui.texteditor.ITextEditor; import com.microsoft.copilot.eclipse.core.Constants; +import com.microsoft.copilot.eclipse.core.CopilotCore; import com.microsoft.copilot.eclipse.core.completion.CompletionCollection; import com.microsoft.copilot.eclipse.core.completion.CompletionProvider; import com.microsoft.copilot.eclipse.core.logger.LogLevel; @@ -56,23 +57,23 @@ public CompletionHandler(CopilotLanguageServerConnection lsConnection, Completio // if the text viewer is null, we will not register listeners. // the side effect is that the completion will not be triggered for this editor. if (textViewer == null) { - CopilotUi.LOGGER.log(LogLevel.INFO, "Text viewer is null for editor: " + editor.getTitle()); + CopilotCore.LOGGER.log(LogLevel.INFO, "Text viewer is null for editor: " + editor.getTitle()); return; } this.document = LSPEclipseUtils.getDocument(editor); if (this.document == null) { - CopilotUi.LOGGER.log(LogLevel.INFO, "Document is null for editor: " + editor.getTitle()); + CopilotCore.LOGGER.log(LogLevel.INFO, "Document is null for editor: " + editor.getTitle()); return; } this.documentUri = LSPEclipseUtils.toUri(document); if (this.documentUri == null) { - CopilotUi.LOGGER.log(LogLevel.INFO, "Document URI is null for editor: " + editor.getTitle()); + CopilotCore.LOGGER.log(LogLevel.INFO, "Document URI is null for editor: " + editor.getTitle()); return; } try { lsConnection.connectDocument(this.document); } catch (IOException e) { - CopilotUi.LOGGER.log(LogLevel.ERROR, e); + CopilotCore.LOGGER.log(LogLevel.ERROR, e); return; } this.documentVersion = -1; @@ -111,7 +112,7 @@ public void acceptFullSuggestion() { this.completionManager.acceptSuggestion(); this.document.removePosition(this.triggerPosition); } catch (BadLocationException e) { - CopilotUi.LOGGER.log(LogLevel.ERROR, e); + CopilotCore.LOGGER.log(LogLevel.ERROR, e); return; } this.clearCompletionRendering(); @@ -202,7 +203,7 @@ public void dispose() { try { this.document.removePositionCategory(this.getCategory()); } catch (BadPositionCategoryException e) { - CopilotUi.LOGGER.log(LogLevel.ERROR, e); + CopilotCore.LOGGER.log(LogLevel.ERROR, e); } this.document.removePositionUpdater(this.positionUpdater); SwtUtils.invokeOnDisplayThread(() -> { diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionManager.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionManager.java index 5c76d34d..b38235c1 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionManager.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionManager.java @@ -18,6 +18,7 @@ import org.eclipse.swt.graphics.RGB; import org.eclipse.swt.graphics.Rectangle; +import com.microsoft.copilot.eclipse.core.CopilotCore; import com.microsoft.copilot.eclipse.core.completion.CompletionCollection; import com.microsoft.copilot.eclipse.core.completion.CompletionListener; import com.microsoft.copilot.eclipse.core.completion.CompletionProvider; @@ -78,7 +79,7 @@ public void triggerCompletion(Position position, int documentVersion) { this.provider.triggerCompletion(documentUri.toASCIIString(), LSPEclipseUtils.toPosition(position.getOffset(), this.document), documentVersion); } catch (BadLocationException e) { - CopilotUi.LOGGER.log(LogLevel.ERROR, e); + CopilotCore.LOGGER.log(LogLevel.ERROR, e); } } @@ -97,7 +98,7 @@ public void clearGhostText() { int offset = LSPEclipseUtils.toOffset(this.completions.getTriggerPosition(), this.document); this.triggerPosition = new Position(offset); } catch (BadLocationException e) { - CopilotUi.LOGGER.log(LogLevel.ERROR, e); + CopilotCore.LOGGER.log(LogLevel.ERROR, e); return; } this.completions = null; diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/dialogs/SignInConfirmDialog.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/dialogs/SignInConfirmDialog.java index c28b40a6..b58af3f2 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/dialogs/SignInConfirmDialog.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/dialogs/SignInConfirmDialog.java @@ -56,7 +56,7 @@ public void run() { try { this.run(true, true, task); } catch (Exception e) { - CopilotUi.LOGGER.log(LogLevel.ERROR, e); + CopilotCore.LOGGER.log(LogLevel.ERROR, e); } } @@ -77,7 +77,7 @@ public void run(IProgressMonitor monitor) throws InvocationTargetException, Inte try { return CopilotCore.getPlugin().getAuthStatusManager().signInConfirm(userCode); } catch (Exception e) { - CopilotUi.LOGGER.log(LogLevel.ERROR, e); + CopilotCore.LOGGER.log(LogLevel.ERROR, e); return null; } }); diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/ShowStatusBarMenuHandler.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/ShowStatusBarMenuHandler.java index ceaa4a5d..d0a706ad 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/ShowStatusBarMenuHandler.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/ShowStatusBarMenuHandler.java @@ -185,7 +185,7 @@ public void run() { try { handlerService.executeCommand(commandId, null); } catch (Exception e) { - CopilotUi.LOGGER.log(LogLevel.ERROR, e); + CopilotCore.LOGGER.log(LogLevel.ERROR, e); } } }; diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/SignInHandler.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/SignInHandler.java index fdc35e98..55eea76e 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/SignInHandler.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/SignInHandler.java @@ -72,9 +72,9 @@ protected IStatus run(IProgressMonitor monitor) { String msg = Messages.signInHandler_msgDialog_signInFailed; if (StringUtils.isNotBlank(e.getMessage())) { msg += " " + e.getMessage(); - CopilotUi.LOGGER.log(LogLevel.ERROR, msg, e); + CopilotCore.LOGGER.log(LogLevel.ERROR, msg, e); } - + String errorMsg = "Sign in failed: " + e.getMessage(); return new Status(IStatus.ERROR, Constants.PLUGIN_ID, errorMsg); } diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/SignOutHandler.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/SignOutHandler.java index 2c446363..180d5857 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/SignOutHandler.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/SignOutHandler.java @@ -49,7 +49,7 @@ private void handleSignOutException(Shell shell, Exception e) { String msg = Messages.signOutHandler_msgDialog_signOutFailed; if (StringUtils.isNotBlank(e.getMessage())) { msg += " " + e.getMessage(); - CopilotUi.LOGGER.log(LogLevel.ERROR, e); + CopilotCore.LOGGER.log(LogLevel.ERROR, e); } MessageDialog.openError(shell, Messages.signOutHandler_msgDialog_signOutFailedFailure, msg); } diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/ViewFeedbackForumHandler.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/ViewFeedbackForumHandler.java index 9831fc82..e013f785 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/ViewFeedbackForumHandler.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/ViewFeedbackForumHandler.java @@ -4,6 +4,7 @@ import org.eclipse.core.commands.ExecutionEvent; import org.eclipse.core.commands.ExecutionException; +import com.microsoft.copilot.eclipse.core.CopilotCore; import com.microsoft.copilot.eclipse.core.logger.LogLevel; import com.microsoft.copilot.eclipse.ui.CopilotUi; import com.microsoft.copilot.eclipse.ui.UiConstants; @@ -19,7 +20,7 @@ public Object execute(ExecutionEvent event) throws ExecutionException { try { UiUtils.openLink(UiConstants.COPILOT_FEEDBACK_FORUM_URL); } catch (Exception e) { - CopilotUi.LOGGER.log(LogLevel.ERROR, e); + CopilotCore.LOGGER.log(LogLevel.ERROR, e); } return null; diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/utils/UiUtils.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/utils/UiUtils.java index 87448fa7..9ddc7f0c 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/utils/UiUtils.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/utils/UiUtils.java @@ -20,6 +20,7 @@ import org.eclipse.ui.menus.UIElement; import org.eclipse.ui.texteditor.ITextEditor; +import com.microsoft.copilot.eclipse.core.CopilotCore; import com.microsoft.copilot.eclipse.core.logger.LogLevel; import com.microsoft.copilot.eclipse.core.utils.PlatformUtils; import com.microsoft.copilot.eclipse.ui.CopilotUi; @@ -57,7 +58,7 @@ public static boolean openLink(String link) { IWebBrowser browser = browserSupport.createBrowser(IWorkbenchBrowserSupport.AS_EXTERNAL, null, null, null); browser.openURL(new URI(encodedUrl).toURL()); } catch (Exception e) { - CopilotUi.LOGGER.log(LogLevel.ERROR, e); + CopilotCore.LOGGER.log(LogLevel.ERROR, e); return false; } return true; From 6984f8ef0984448a0a1ba8de1db7b9dcb60bacfb Mon Sep 17 00:00:00 2001 From: Sheng Chen Date: Mon, 6 Jan 2025 09:54:25 +0800 Subject: [PATCH 051/690] refactor - Separate document events handles and ghost text rendering (#85) --- .../ui/completion/CompletionManagerTests.java | 9 +- .../ui/completion/EditorManagerTests.java | 18 +- .../AcceptFullSuggestionHandlerTests.java | 16 +- .../DiscardSuggestionHandlerTests.java | 16 +- .../TriggerInlineSuggestionHandlerTests.java | 8 +- com.microsoft.copilot.eclipse.ui/plugin.xml | 8 + .../copilot/eclipse/ui/UiConstants.java | 4 +- .../ui/completion/CompletionHandler.java | 217 ----------- .../ui/completion/CompletionManager.java | 355 ++++++++++++------ .../completion/EditorLifecycleListener.java | 6 +- .../eclipse/ui/completion/EditorsManager.java | 36 +- .../eclipse/ui/completion/EolGhostText.java | 32 ++ .../eclipse/ui/completion/GhostText.java | 19 + .../eclipse/ui/completion/GhostTextType.java | 16 + .../ui/completion/RenderingManager.java | 102 +++++ .../completion/codemining/BlockGhostText.java | 23 ++ .../codemining/GhostTextProvider.java | 52 +++ .../handlers/AcceptFullSuggestionHandler.java | 10 +- .../eclipse/ui/handlers/CopilotHandler.java | 8 +- .../ui/handlers/DiscardSuggestionHandler.java | 10 +- .../TriggerInlineSuggestionHandler.java | 14 +- 21 files changed, 572 insertions(+), 407 deletions(-) delete mode 100644 com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionHandler.java create mode 100644 com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/EolGhostText.java create mode 100644 com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/GhostText.java create mode 100644 com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/GhostTextType.java create mode 100644 com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/RenderingManager.java create mode 100644 com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/codemining/BlockGhostText.java create mode 100644 com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/codemining/GhostTextProvider.java diff --git a/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/completion/CompletionManagerTests.java b/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/completion/CompletionManagerTests.java index e40314ee..d0b40822 100644 --- a/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/completion/CompletionManagerTests.java +++ b/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/completion/CompletionManagerTests.java @@ -12,9 +12,8 @@ import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.IResource; import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.jface.preference.PreferenceStore; import org.eclipse.jface.text.IDocument; -import org.eclipse.jface.text.ITextOperationTarget; -import org.eclipse.jface.text.ITextViewer; import org.eclipse.lsp4j.Position; import org.eclipse.lsp4j.Range; import org.eclipse.ui.IEditorPart; @@ -75,12 +74,11 @@ public void hi() { assertTrue(editorPart instanceof ITextEditor); ITextEditor textEditor = (ITextEditor) editorPart; - ITextViewer textViewer = (ITextViewer) textEditor.getAdapter(ITextOperationTarget.class); IDocument document = textEditor.getDocumentProvider().getDocument(textEditor.getEditorInput()); when(mockLsConnection.getDocumentVersion(any())).thenReturn(documentVersion); - CompletionManager manager = new CompletionManager(mockLsConnection, mock(CompletionProvider.class), textViewer, - document, file.getLocationURI()); + CompletionManager manager = new CompletionManager(mockLsConnection, mock(CompletionProvider.class), textEditor, + mock(PreferenceStore.class)); List completions = List.of(new CompletionItem("uuid", " System.out.println(\"hi\");", new Range(new Position(3, 0), new Position(3, 27)), "hi\");", new Position(3, 24), documentVersion)); @@ -103,6 +101,7 @@ protected IEditorPart getEditorPartFor(IFile file) { ref.set(window.getActivePage().openEditor(new org.eclipse.ui.part.FileEditorInput(file), "org.eclipse.ui.DefaultTextEditor")); } catch (PartInitException e) { + // do nothing } } }); diff --git a/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/completion/EditorManagerTests.java b/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/completion/EditorManagerTests.java index a4338eb8..8dc8904d 100644 --- a/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/completion/EditorManagerTests.java +++ b/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/completion/EditorManagerTests.java @@ -27,36 +27,36 @@ class EditorManagerTests { private IPreferenceStore mockPreferenceStore; @Test - void testCreateHandlerForNull() { + void testCreateManagerForNull() { EditorsManager manager = new EditorsManager(mockServer, mockProvider, mockPreferenceStore); - assertNull(manager.getOrCreateCompletionHandlerFor(null)); + assertNull(manager.getOrCreateCompletionManagerFor(null)); } @Test - void testGetOrCreateCompletionHandlerForReturnsNewHandlerWhenNotPresent() { + void testGetOrCreateCompletionManagerForReturnsNewHandlerWhenNotPresent() { ITextEditor mockEditor = mock(ITextEditor.class); EditorsManager manager = new EditorsManager(mockServer, mockProvider, mockPreferenceStore); - CompletionHandler handler = manager.getOrCreateCompletionHandlerFor(mockEditor); + CompletionManager completionManager = manager.getOrCreateCompletionManagerFor(mockEditor); - assertNotNull(handler); + assertNotNull(completionManager); } @Test - void testGetActiveHandlerWhenNoActiveEditor() { + void testGetActiveManagerWhenNoActiveEditor() { EditorsManager manager = new EditorsManager(mockServer, mockProvider, mockPreferenceStore); - assertNull(manager.getActiveCompletionHandler()); + assertNull(manager.getActiveCompletionManager()); } @Test void testGetActiveHandlerWhenActiveEditor() { ITextEditor mockEditor = mock(ITextEditor.class); EditorsManager manager = new EditorsManager(mockServer, mockProvider, mockPreferenceStore); - manager.getOrCreateCompletionHandlerFor(mockEditor); + manager.getOrCreateCompletionManagerFor(mockEditor); manager.setActiveEditor(mockEditor); - assertNotNull(manager.getActiveCompletionHandler()); + assertNotNull(manager.getActiveCompletionManager()); } } diff --git a/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/handler/AcceptFullSuggestionHandlerTests.java b/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/handler/AcceptFullSuggestionHandlerTests.java index ebf06b18..e8126d7a 100644 --- a/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/handler/AcceptFullSuggestionHandlerTests.java +++ b/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/handler/AcceptFullSuggestionHandlerTests.java @@ -21,7 +21,7 @@ import com.microsoft.copilot.eclipse.core.lsp.CopilotLanguageServerConnection; import com.microsoft.copilot.eclipse.core.lsp.protocol.CompletionItem; import com.microsoft.copilot.eclipse.ui.CopilotUi; -import com.microsoft.copilot.eclipse.ui.completion.CompletionHandler; +import com.microsoft.copilot.eclipse.ui.completion.CompletionManager; import com.microsoft.copilot.eclipse.ui.completion.EditorsManager; import com.microsoft.copilot.eclipse.ui.handlers.AcceptFullSuggestionHandler; @@ -32,10 +32,10 @@ class AcceptFullSuggestionHandlerTests { void testIsNotEnabledWhenNoCompletionIsAvailable() { CopilotUi mockedUi = mock(CopilotUi.class); EditorsManager mockedManager = mock(EditorsManager.class); - CompletionHandler mockedHandler = mock(CompletionHandler.class); + CompletionManager mockedCompletionManager = mock(CompletionManager.class); when(mockedUi.getEditorsManager()).thenReturn(mockedManager); - when(mockedManager.getActiveCompletionHandler()).thenReturn(mockedHandler); - when(mockedHandler.hasCompletion()).thenReturn(false); + when(mockedManager.getActiveCompletionManager()).thenReturn(mockedCompletionManager); + when(mockedCompletionManager.hasCompletion()).thenReturn(false); AcceptFullSuggestionHandler handler = new AcceptFullSuggestionHandler(); @@ -54,11 +54,11 @@ void testAcceptionNotifiedWhenCompletionIsAccepted() throws ExecutionException { CompletionCollection completions = new CompletionCollection( List.of(new CompletionItem("uuid", "text", null, "displayText", null, 0)), "uri"); - CompletionHandler mockedHandler = mock(CompletionHandler.class); - doNothing().when(mockedHandler).acceptFullSuggestion(); - when(mockedHandler.getCompletions()).thenReturn(completions); + CompletionManager mockedCompletionManager = mock(CompletionManager.class); + doNothing().when(mockedCompletionManager).acceptFullSuggestion(); + when(mockedCompletionManager.getCompletions()).thenReturn(completions); EditorsManager mockedManager = mock(EditorsManager.class); - when(mockedManager.getActiveCompletionHandler()).thenReturn(mockedHandler); + when(mockedManager.getActiveCompletionManager()).thenReturn(mockedCompletionManager); CopilotUi mockedUi = mock(CopilotUi.class); when(mockedUi.getEditorsManager()).thenReturn(mockedManager); diff --git a/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/handler/DiscardSuggestionHandlerTests.java b/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/handler/DiscardSuggestionHandlerTests.java index 97061c0c..61931f91 100644 --- a/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/handler/DiscardSuggestionHandlerTests.java +++ b/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/handler/DiscardSuggestionHandlerTests.java @@ -20,7 +20,7 @@ import com.microsoft.copilot.eclipse.core.completion.CompletionCollection; import com.microsoft.copilot.eclipse.core.lsp.CopilotLanguageServerConnection; import com.microsoft.copilot.eclipse.ui.CopilotUi; -import com.microsoft.copilot.eclipse.ui.completion.CompletionHandler; +import com.microsoft.copilot.eclipse.ui.completion.CompletionManager; import com.microsoft.copilot.eclipse.ui.completion.EditorsManager; import com.microsoft.copilot.eclipse.ui.handlers.DiscardSuggestionHandler; @@ -31,10 +31,10 @@ class DiscardSuggestionHandlerTests { void testIsNotEnabledWhenNoCompletionIsAvailable() { CopilotUi mockedUi = mock(CopilotUi.class); EditorsManager mockedManager = mock(EditorsManager.class); - CompletionHandler mockedHandler = mock(CompletionHandler.class); + CompletionManager mockedCompletionManager = mock(CompletionManager.class); when(mockedUi.getEditorsManager()).thenReturn(mockedManager); - when(mockedManager.getActiveCompletionHandler()).thenReturn(mockedHandler); - when(mockedHandler.hasCompletion()).thenReturn(false); + when(mockedManager.getActiveCompletionManager()).thenReturn(mockedCompletionManager); + when(mockedCompletionManager.hasCompletion()).thenReturn(false); DiscardSuggestionHandler handler = new DiscardSuggestionHandler(); @@ -53,11 +53,11 @@ void testRejectionNotifiedWhenCompletionIsDiscarded() throws ExecutionException CompletionCollection completions = mock(CompletionCollection.class); when(completions.getUuids()).thenReturn(List.of("uuid")); - CompletionHandler mockedHandler = mock(CompletionHandler.class); - doNothing().when(mockedHandler).clearCompletionRendering(); - when(mockedHandler.getCompletions()).thenReturn(completions); + CompletionManager mockedCompletionManager = mock(CompletionManager.class); + doNothing().when(mockedCompletionManager).clearCompletionRendering(); + when(mockedCompletionManager.getCompletions()).thenReturn(completions); EditorsManager mockedManager = mock(EditorsManager.class); - when(mockedManager.getActiveCompletionHandler()).thenReturn(mockedHandler); + when(mockedManager.getActiveCompletionManager()).thenReturn(mockedCompletionManager); CopilotUi mockedUi = mock(CopilotUi.class); when(mockedUi.getEditorsManager()).thenReturn(mockedManager); diff --git a/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/handler/TriggerInlineSuggestionHandlerTests.java b/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/handler/TriggerInlineSuggestionHandlerTests.java index 2aaec16e..db0e90ce 100644 --- a/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/handler/TriggerInlineSuggestionHandlerTests.java +++ b/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/handler/TriggerInlineSuggestionHandlerTests.java @@ -11,7 +11,7 @@ import org.mockito.junit.jupiter.MockitoExtension; import com.microsoft.copilot.eclipse.ui.CopilotUi; -import com.microsoft.copilot.eclipse.ui.completion.CompletionHandler; +import com.microsoft.copilot.eclipse.ui.completion.CompletionManager; import com.microsoft.copilot.eclipse.ui.completion.EditorsManager; import com.microsoft.copilot.eclipse.ui.handlers.TriggerInlineSuggestionHandler; @@ -22,10 +22,10 @@ class TriggerInlineSuggestionHandlerTests { void testIsEnabledWhenNoCompletionIsAvailable() { CopilotUi mockedUi = mock(CopilotUi.class); EditorsManager mockedManager = mock(EditorsManager.class); - CompletionHandler mockedHandler = mock(CompletionHandler.class); + CompletionManager mockedCompletionManager = mock(CompletionManager.class); when(mockedUi.getEditorsManager()).thenReturn(mockedManager); - when(mockedManager.getActiveCompletionHandler()).thenReturn(mockedHandler); - when(mockedHandler.hasCompletion()).thenReturn(false); + when(mockedManager.getActiveCompletionManager()).thenReturn(mockedCompletionManager); + when(mockedCompletionManager.hasCompletion()).thenReturn(false); TriggerInlineSuggestionHandler handler = new TriggerInlineSuggestionHandler(); diff --git a/com.microsoft.copilot.eclipse.ui/plugin.xml b/com.microsoft.copilot.eclipse.ui/plugin.xml index 9364b327..23eb5f58 100644 --- a/com.microsoft.copilot.eclipse.ui/plugin.xml +++ b/com.microsoft.copilot.eclipse.ui/plugin.xml @@ -155,4 +155,12 @@ sequence="ESC"> + + + + + diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/UiConstants.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/UiConstants.java index da347bad..72de0ca2 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/UiConstants.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/UiConstants.java @@ -16,8 +16,8 @@ private UiConstants() { * Default color scale for ghost text. */ - public static final int DEFAULT_GHOST_TEXT_SCALE = 112; - + public static final int DEFAULT_GHOST_TEXT_SCALE = 128; + /** * The URL constants for the Copilot menu. */ diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionHandler.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionHandler.java deleted file mode 100644 index e7056b38..00000000 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionHandler.java +++ /dev/null @@ -1,217 +0,0 @@ -package com.microsoft.copilot.eclipse.ui.completion; - -import java.io.IOException; -import java.net.URI; - -import org.eclipse.jface.preference.IPreferenceStore; -import org.eclipse.jface.text.BadLocationException; -import org.eclipse.jface.text.BadPositionCategoryException; -import org.eclipse.jface.text.DefaultPositionUpdater; -import org.eclipse.jface.text.IDocument; -import org.eclipse.jface.text.ITextOperationTarget; -import org.eclipse.jface.text.ITextViewer; -import org.eclipse.jface.text.TextSelection; -import org.eclipse.jface.util.IPropertyChangeListener; -import org.eclipse.jface.util.PropertyChangeEvent; -import org.eclipse.lsp4e.LSPEclipseUtils; -import org.eclipse.swt.custom.CaretEvent; -import org.eclipse.swt.custom.CaretListener; -import org.eclipse.ui.texteditor.ITextEditor; - -import com.microsoft.copilot.eclipse.core.Constants; -import com.microsoft.copilot.eclipse.core.CopilotCore; -import com.microsoft.copilot.eclipse.core.completion.CompletionCollection; -import com.microsoft.copilot.eclipse.core.completion.CompletionProvider; -import com.microsoft.copilot.eclipse.core.logger.LogLevel; -import com.microsoft.copilot.eclipse.core.lsp.CopilotLanguageServerConnection; -import com.microsoft.copilot.eclipse.ui.CopilotUi; -import com.microsoft.copilot.eclipse.ui.utils.SwtUtils; -import com.microsoft.copilot.eclipse.ui.utils.UiUtils; - -/** - * A class to listen events which are completion related and notify the completion manager to render the ghost text or - * apply the suggestion to document. - */ -public class CompletionHandler implements CaretListener, IPropertyChangeListener { - - private CopilotLanguageServerConnection lsConnection; - private CompletionProvider provider; - private ITextViewer textViewer; - private IDocument document; - private URI documentUri; - private int documentVersion; - private org.eclipse.jface.text.Position triggerPosition; - - private DefaultPositionUpdater positionUpdater; - private CompletionManager completionManager; - private boolean autoShowCompletion; - private IPreferenceStore preferenceStore; - - /** - * Creates a new completion handler. - */ - public CompletionHandler(CopilotLanguageServerConnection lsConnection, CompletionProvider provider, - ITextEditor editor, IPreferenceStore preferenceStore) { - this.lsConnection = lsConnection; - this.textViewer = (ITextViewer) editor.getAdapter(ITextOperationTarget.class); - // if the text viewer is null, we will not register listeners. - // the side effect is that the completion will not be triggered for this editor. - if (textViewer == null) { - CopilotCore.LOGGER.log(LogLevel.INFO, "Text viewer is null for editor: " + editor.getTitle()); - return; - } - this.document = LSPEclipseUtils.getDocument(editor); - if (this.document == null) { - CopilotCore.LOGGER.log(LogLevel.INFO, "Document is null for editor: " + editor.getTitle()); - return; - } - this.documentUri = LSPEclipseUtils.toUri(document); - if (this.documentUri == null) { - CopilotCore.LOGGER.log(LogLevel.INFO, "Document URI is null for editor: " + editor.getTitle()); - return; - } - try { - lsConnection.connectDocument(this.document); - } catch (IOException e) { - CopilotCore.LOGGER.log(LogLevel.ERROR, e); - return; - } - this.documentVersion = -1; - this.triggerPosition = new org.eclipse.jface.text.Position(0); - this.completionManager = new CompletionManager(lsConnection, provider, this.textViewer, this.document, - this.documentUri); - registerListeners(); - - // position updater is used to update the position when the document is changed. - // this is needed because the completion ghost text is rendered based on the - // position in the document. If the document is changed, the position will be - // invalidated. - this.positionUpdater = new DefaultPositionUpdater(this.getCategory()); - this.document.addPositionCategory(this.getCategory()); - this.document.addPositionUpdater(this.positionUpdater); - - // initialize the auto show completion preference and add listener to update it. - this.preferenceStore = preferenceStore; - this.autoShowCompletion = preferenceStore.getBoolean(Constants.AUTO_SHOW_COMPLETION); - preferenceStore.addPropertyChangeListener(this); - } - - /** - * Check if the completion handler has any completion suggestions. - */ - public boolean hasCompletion() { - return this.completionManager.hasCompletion(); - } - - /** - * Accept the full completion suggestion. - */ - public void acceptFullSuggestion() { - try { - this.document.addPosition(this.triggerPosition); - this.completionManager.acceptSuggestion(); - this.document.removePosition(this.triggerPosition); - } catch (BadLocationException e) { - CopilotCore.LOGGER.log(LogLevel.ERROR, e); - return; - } - this.clearCompletionRendering(); - SwtUtils.invokeOnDisplayThread(() -> { - this.textViewer.getSelectionProvider().setSelection(new TextSelection(this.triggerPosition.offset, 0)); - }, this.textViewer.getTextWidget()); - } - - void registerListeners() { - SwtUtils.invokeOnDisplayThread(() -> { - this.textViewer.getTextWidget().addCaretListener(this); - }); - } - - /** - * Trigger the inline completion. - */ - public void triggerCompletion() { - clearCompletionRendering(); - this.completionManager.triggerCompletion(this.triggerPosition, this.documentVersion); - } - - /** - * Clear the completion ghost text. - */ - public void clearCompletionRendering() { - this.completionManager.clearGhostText(); - } - - public CompletionCollection getCompletions() { - return this.completionManager.getCompletions(); - } - - @Override - public void caretMoved(CaretEvent event) { - int modelOffset = UiUtils.widgetOffset2ModelOffset(textViewer, event.caretOffset); - this.triggerPosition = new org.eclipse.jface.text.Position(modelOffset); - - // it's guaranteed that the document change event comes earlier than caret - // change event. See org.eclipse.swt.custom.StyledText#modifyContent() - int currentVersion = this.lsConnection.getDocumentVersion(this.documentUri); - - // initialize the document version and return. This avoids the ghost text - // being rendered when user opens the editor and just clicks in it. - if (this.documentVersion < 0) { - this.documentVersion = currentVersion; - return; - } - if (currentVersion == this.documentVersion) { - // if the caret position is changed without document version change, we should remove the ghost text. - clearCompletionRendering(); - } else { - this.documentVersion = currentVersion; - if (this.autoShowCompletion) { - triggerCompletion(); - } - } - - } - - @Override - public void propertyChange(PropertyChangeEvent event) { - if (event.getProperty().equals(Constants.AUTO_SHOW_COMPLETION)) { - this.autoShowCompletion = Boolean.parseBoolean(event.getNewValue().toString()); - } - } - - /** - * Get category for the position updater of this document. - */ - private String getCategory() { - return "GCE-" + this.documentUri.toASCIIString(); - } - - /** - * Disposes the resources of this completion handler. - */ - public void dispose() { - if (this.completionManager == null) { - // null manager means the handler is not initialized. - return; - } - - this.completionManager.dispose(); - this.completionManager = null; - preferenceStore.removePropertyChangeListener(this); - lsConnection.disconnectDocument(this.documentUri); - try { - this.document.removePositionCategory(this.getCategory()); - } catch (BadPositionCategoryException e) { - CopilotCore.LOGGER.log(LogLevel.ERROR, e); - } - this.document.removePositionUpdater(this.positionUpdater); - SwtUtils.invokeOnDisplayThread(() -> { - if (this.textViewer.getTextWidget() != null) { - this.textViewer.getTextWidget().removeCaretListener(this); - } - }); - - } - -} diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionManager.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionManager.java index b38235c1..d2756521 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionManager.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/CompletionManager.java @@ -1,23 +1,32 @@ package com.microsoft.copilot.eclipse.ui.completion; +import java.io.IOException; import java.net.URI; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import java.util.Objects; import org.apache.commons.lang3.StringUtils; +import org.eclipse.jface.preference.IPreferenceStore; import org.eclipse.jface.text.BadLocationException; +import org.eclipse.jface.text.BadPositionCategoryException; +import org.eclipse.jface.text.DefaultPositionUpdater; import org.eclipse.jface.text.IDocument; +import org.eclipse.jface.text.ITextOperationTarget; import org.eclipse.jface.text.ITextViewer; -import org.eclipse.jface.text.Position; +import org.eclipse.jface.text.TextSelection; +import org.eclipse.jface.text.codemining.ICodeMining; +import org.eclipse.jface.text.source.ISourceViewerExtension5; +import org.eclipse.jface.util.IPropertyChangeListener; +import org.eclipse.jface.util.PropertyChangeEvent; import org.eclipse.lsp4e.LSPEclipseUtils; +import org.eclipse.swt.custom.CaretEvent; +import org.eclipse.swt.custom.CaretListener; import org.eclipse.swt.custom.StyledText; -import org.eclipse.swt.events.PaintEvent; -import org.eclipse.swt.events.PaintListener; -import org.eclipse.swt.graphics.Color; -import org.eclipse.swt.graphics.GC; -import org.eclipse.swt.graphics.Point; -import org.eclipse.swt.graphics.RGB; -import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.ui.texteditor.ITextEditor; +import com.microsoft.copilot.eclipse.core.Constants; import com.microsoft.copilot.eclipse.core.CopilotCore; import com.microsoft.copilot.eclipse.core.completion.CompletionCollection; import com.microsoft.copilot.eclipse.core.completion.CompletionListener; @@ -26,87 +35,123 @@ import com.microsoft.copilot.eclipse.core.lsp.CopilotLanguageServerConnection; import com.microsoft.copilot.eclipse.core.lsp.protocol.CompletionItem; import com.microsoft.copilot.eclipse.core.lsp.protocol.NotifyShownParams; -import com.microsoft.copilot.eclipse.ui.CopilotUi; -import com.microsoft.copilot.eclipse.ui.UiConstants; +import com.microsoft.copilot.eclipse.ui.completion.codemining.BlockGhostText; import com.microsoft.copilot.eclipse.ui.utils.SwtUtils; import com.microsoft.copilot.eclipse.ui.utils.UiUtils; /** - * A class to control completion rendering. + * A class to listen events which are completion related and notify the completion manager to render the ghost text or + * apply the suggestion to document. */ -public class CompletionManager implements CompletionListener, PaintListener { +public class CompletionManager implements CaretListener, CompletionListener, IPropertyChangeListener { private CopilotLanguageServerConnection lsConnection; private CompletionProvider provider; + private CompletionCollection completions; + private ITextViewer textViewer; + private StyledText styledText; private IDocument document; private URI documentUri; - private CompletionCollection completions; + private int documentVersion; + private org.eclipse.jface.text.Position triggerPosition; + private List codeMinings; - private ITextViewer textViewer; - private Color ghostTextColor; - private Position triggerPosition; + private DefaultPositionUpdater positionUpdater; + private RenderingManager renderingManager; + private boolean autoShowCompletion; + private IPreferenceStore preferenceStore; /** - * Creates a new CompletionManager. + * Creates a new completion manager. The manager is responsible for trigger the completion, apply suggestions to the + * document. And schedule the rendering of ghost text. */ public CompletionManager(CopilotLanguageServerConnection lsConnection, CompletionProvider provider, - ITextViewer textViewer, IDocument document, URI documentUri) { + ITextEditor editor, IPreferenceStore preferenceStore) { + this.codeMinings = new ArrayList<>(); + this.textViewer = (ITextViewer) editor.getAdapter(ITextOperationTarget.class); + // if the text viewer is null, we will not register listeners. + // the side effect is that the completion will not be triggered for this editor. + if (textViewer == null) { + CopilotCore.LOGGER.log(LogLevel.INFO, "Text viewer is null for editor: " + editor.getTitle()); + return; + } + this.styledText = this.textViewer.getTextWidget(); + if (this.styledText == null) { + CopilotCore.LOGGER.log(LogLevel.INFO, "Styled text is null for editor: " + editor.getTitle()); + return; + } + this.document = LSPEclipseUtils.getDocument(editor); + if (this.document == null) { + CopilotCore.LOGGER.log(LogLevel.INFO, "Document is null for editor: " + editor.getTitle()); + return; + } + this.documentUri = LSPEclipseUtils.toUri(document); + if (this.documentUri == null) { + CopilotCore.LOGGER.log(LogLevel.INFO, "Document URI is null for editor: " + editor.getTitle()); + return; + } + try { + lsConnection.connectDocument(this.document); + } catch (IOException e) { + CopilotCore.LOGGER.log(LogLevel.ERROR, e); + return; + } + + this.renderingManager = new RenderingManager(this.textViewer); + this.lsConnection = lsConnection; this.provider = provider; this.provider.addCompletionListener(this); - this.document = document; - this.documentUri = documentUri; this.completions = null; + this.documentVersion = -1; + this.triggerPosition = new org.eclipse.jface.text.Position(0); - this.triggerPosition = new Position(0); - this.textViewer = textViewer; - StyledText styledText = textViewer.getTextWidget(); - if (styledText != null) { - SwtUtils.invokeOnDisplayThread(() -> { - styledText.addPaintListener(this); - this.ghostTextColor = new Color(styledText.getDisplay(), new RGB(UiConstants.DEFAULT_GHOST_TEXT_SCALE, - UiConstants.DEFAULT_GHOST_TEXT_SCALE, UiConstants.DEFAULT_GHOST_TEXT_SCALE)); - }, styledText); - } + // initialize the auto show completion preference and add listener to update it. + this.preferenceStore = preferenceStore; + this.autoShowCompletion = preferenceStore.getBoolean(Constants.AUTO_SHOW_COMPLETION); + + // position updater is used to update the position when the document is changed. + // this is needed because the completion ghost text is rendered based on the + // position in the document. If the document is changed, the position will be + // invalidated. + this.positionUpdater = new DefaultPositionUpdater(this.getCategory()); + this.document.addPositionCategory(this.getCategory()); + this.document.addPositionUpdater(this.positionUpdater); + + registerListeners(); } - /** - * Triggers the completion. - */ - public void triggerCompletion(Position position, int documentVersion) { - this.triggerPosition = position; - try { - this.provider.triggerCompletion(documentUri.toASCIIString(), - LSPEclipseUtils.toPosition(position.getOffset(), this.document), documentVersion); - } catch (BadLocationException e) { - CopilotCore.LOGGER.log(LogLevel.ERROR, e); - } + void registerListeners() { + SwtUtils.invokeOnDisplayThread(() -> { + this.styledText.addCaretListener(this); + }, this.styledText); + + this.preferenceStore.addPropertyChangeListener(this); } - /** - * Clear the completion. - */ - public void clearGhostText() { - if (this.completions == null || this.completions.getSize() == 0) { - return; - } - try { - // use completion trigger position if available. this.triggerPosition is the current - // cursor position, which may not be the same as the completion trigger position when user - // use mouse to move the cursor. In that case, the line vertical indentation might not be - // reset correctly. - int offset = LSPEclipseUtils.toOffset(this.completions.getTriggerPosition(), this.document); - this.triggerPosition = new Position(offset); - } catch (BadLocationException e) { - CopilotCore.LOGGER.log(LogLevel.ERROR, e); + @Override + public void caretMoved(CaretEvent event) { + int modelOffset = UiUtils.widgetOffset2ModelOffset(textViewer, event.caretOffset); + this.triggerPosition = new org.eclipse.jface.text.Position(modelOffset); + + // it's guaranteed that the document change event comes earlier than caret + // change event. See org.eclipse.swt.custom.StyledText#modifyContent() + int currentVersion = this.lsConnection.getDocumentVersion(this.documentUri); + + // initialize the document version and return. This avoids the ghost text + // being rendered when user opens the editor and just clicks in it. + if (this.documentVersion < 0) { + this.documentVersion = currentVersion; return; } - this.completions = null; - StyledText styledText = textViewer.getTextWidget(); - if (styledText != null) { - this.setLineVerticalIndentation(styledText, null, - UiUtils.modelOffset2WidgetOffset(textViewer, this.triggerPosition.getOffset())); - SwtUtils.invokeOnDisplayThread(styledText::redraw, styledText); + if (currentVersion == this.documentVersion) { + // if the caret position is changed without document version change, we should remove the ghost text. + clearCompletionRendering(); + } else { + this.documentVersion = currentVersion; + if (this.autoShowCompletion) { + triggerCompletion(); + } } } @@ -122,71 +167,103 @@ public void onCompletionResolved(CompletionCollection completions) { } this.completions = completions; - StyledText styledText = textViewer.getTextWidget(); - if (styledText != null) { - SwtUtils.invokeOnDisplayThread(styledText::redraw, styledText); - this.notifyShown(); - } - } - @Override - public void paintControl(PaintEvent e) { - StyledText styledText = textViewer.getTextWidget(); - if (styledText == null) { - return; + // render the first line by ourself to make sure cursor position not change. + List ghostTexts = resolveGhostTexts(); + if (!ghostTexts.isEmpty()) { + this.renderingManager.setGhostTexts(ghostTexts); + this.renderingManager.redraw(); + this.notifyShown(); } - GC gc = e.gc; - int widgetOffset = UiUtils.modelOffset2WidgetOffset(textViewer, this.triggerPosition.getOffset()); - // will get index out of bounds if the cursor is at the end. - // Because there is no more text to get bounds at EOF. - widgetOffset = Math.max(Math.min(widgetOffset, styledText.getCharCount() - 1), 0); - setLineVerticalIndentation(styledText, gc, widgetOffset); + // render the remaining lines by code mining api. + resolveCodeMiningGhostTexts(); + this.updateCodeMinings(); + } - if (this.completions == null) { - return; + private List resolveGhostTexts() { + if (this.completions == null || this.completions.getSize() == 0) { + return Collections.emptyList(); } - - gc.setForeground(this.ghostTextColor); + List ghostTexts = new ArrayList<>(); String firstLine = this.completions.getFirstLine(); if (StringUtils.isNotBlank(firstLine)) { - Rectangle bounds = styledText.getTextBounds(widgetOffset, widgetOffset); - int y = bounds.y; - y += bounds.height - styledText.getLineHeight(); - gc.drawString(firstLine, bounds.x + bounds.width, y, true); + ghostTexts.add(new EolGhostText(firstLine, triggerPosition.getOffset())); + } + return ghostTexts; + } + + private void resolveCodeMiningGhostTexts() { + if (this.completions == null || this.completions.getSize() == 0) { + this.codeMinings.clear(); + return; } + List cm = new ArrayList<>(); String remainingLines = this.completions.getRemainingLines(); - if (StringUtils.isNotBlank(remainingLines)) { - int lineHeight = styledText.getLineHeight(); - int fontHeight = gc.getFontMetrics().getHeight(); - int x = styledText.getLeftMargin(); - Point offsetLocation = styledText.getLocationAtOffset(widgetOffset); - int y = offsetLocation.y + lineHeight * 2 - fontHeight; - gc.drawText(remainingLines, x, y, true); + if (StringUtils.isNotEmpty(remainingLines)) { + try { + cm.add( + new BlockGhostText(document.getLineOfOffset(triggerPosition.offset) + 1, document, null, remainingLines)); + } catch (BadLocationException e) { + CopilotCore.LOGGER.log(LogLevel.ERROR, e); + } } + this.codeMinings = cm; + } + private void updateCodeMinings() { + if (textViewer instanceof ISourceViewerExtension5 sve) { + sve.updateCodeMinings(); + } } - private void setLineVerticalIndentation(StyledText styledText, GC gc, int widgetOffset) { - int height = 0; - if (this.completions != null && gc != null) { - // Change the height (line vertical indentation) to fit the line of - // ghost text. - Point ghostTextExtent = gc.textExtent(this.completions.getText()); - int numberOfLines = this.completions.getNumberOfLines(); - height = ghostTextExtent.y - ghostTextExtent.y / numberOfLines; + @Override + public void propertyChange(PropertyChangeEvent event) { + if (event.getProperty().equals(Constants.AUTO_SHOW_COMPLETION)) { + this.autoShowCompletion = Boolean.parseBoolean(event.getNewValue().toString()); } + } - int lineIndex = styledText.getLineAtOffset(widgetOffset) + 1; - lineIndex = Math.min(lineIndex, styledText.getLineCount() - 1); - styledText.setLineVerticalIndent(lineIndex, height); + /** + * Trigger the inline completion. + */ + public void triggerCompletion() { + clearCompletionRendering(); + try { + this.provider.triggerCompletion(documentUri.toASCIIString(), + LSPEclipseUtils.toPosition(this.triggerPosition.getOffset(), this.document), documentVersion); + } catch (BadLocationException e) { + CopilotCore.LOGGER.log(LogLevel.ERROR, e); + } } /** - * Return if the completion manager has completion rendering. + * Clear the completion ghost text. */ - public boolean hasCompletion() { - return this.completions != null; + public void clearCompletionRendering() { + this.codeMinings.clear(); + this.updateCodeMinings(); + + this.renderingManager.clearGhostText(); + this.completions = null; + } + + /** + * Accept the full completion suggestion. + */ + public void acceptFullSuggestion() { + try { + this.document.addPosition(this.triggerPosition); + this.acceptSuggestion(); + this.document.removePosition(this.triggerPosition); + } catch (BadLocationException e) { + CopilotCore.LOGGER.log(LogLevel.ERROR, e); + return; + } + this.clearCompletionRendering(); + SwtUtils.invokeOnDisplayThread(() -> { + this.textViewer.getSelectionProvider().setSelection(new TextSelection(this.triggerPosition.offset, 0)); + }, this.textViewer.getTextWidget()); } /** @@ -194,7 +271,7 @@ public boolean hasCompletion() { * * @throws BadLocationException if the offset is invalid. */ - public void acceptSuggestion() throws BadLocationException { + void acceptSuggestion() throws BadLocationException { if (this.completions == null || this.completions.getSize() == 0) { return; } @@ -207,22 +284,60 @@ public void acceptSuggestion() throws BadLocationException { this.document.replace(startOffset, endOffset - startOffset, text); } - public CompletionCollection getCompletions() { - return completions; + /** + * Get category for the position updater of this document. + */ + private String getCategory() { + return "GCE-" + this.documentUri.toASCIIString(); } /** - * Dispose the resources used by the completion manager. + * Disposes the resources of this completion handler. */ public void dispose() { - this.provider.removeCompletionListener(this); - this.completions = null; - if (this.ghostTextColor != null) { - this.ghostTextColor.dispose(); - this.ghostTextColor = null; + if (this.provider != null) { + this.provider.removeCompletionListener(this); + } + if (this.renderingManager != null) { + this.renderingManager.dispose(); + this.renderingManager = null; + } + + if (this.preferenceStore != null) { + preferenceStore.removePropertyChangeListener(this); + } + lsConnection.disconnectDocument(this.documentUri); + + if (this.document != null) { + try { + this.document.removePositionCategory(this.getCategory()); + } catch (BadPositionCategoryException e) { + CopilotCore.LOGGER.log(LogLevel.ERROR, e); + } + this.document.removePositionUpdater(this.positionUpdater); + } + + if (this.styledText != null) { + SwtUtils.invokeOnDisplayThread(() -> { + this.styledText.removeCaretListener(this); + }); } } + /** + * Will be used when notifying the completion rejection/acceptance. + */ + public CompletionCollection getCompletions() { + return this.completions; + } + + /** + * Check if the completion handler has any completion suggestions. + */ + public boolean hasCompletion() { + return this.completions != null; + } + private void notifyShown() { if (this.completions == null || this.completions.getSize() == 0) { return; @@ -237,4 +352,8 @@ private void notifyShown() { this.lsConnection.notifyShown(params); } + public List getCodeMinings() { + return codeMinings; + } + } diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/EditorLifecycleListener.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/EditorLifecycleListener.java index 17f477e7..2557e488 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/EditorLifecycleListener.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/EditorLifecycleListener.java @@ -46,14 +46,14 @@ public void partOpened(IWorkbenchPart part) { } /** - * Creates the {@link CompletionHandler} for the ITextEditor of the IWorkbenchPart. + * Creates the {@link CompletionManager} for the ITextEditor of the IWorkbenchPart. */ public void createCompletionHandlerFor(IWorkbenchPart part) { IEditorPart editorPart = part.getAdapter(IEditorPart.class); if (editorPart != null) { ITextEditor editor = editorPart.getAdapter(ITextEditor.class); if (editor != null) { - manager.getOrCreateCompletionHandlerFor(editor); + manager.getOrCreateCompletionManagerFor(editor); manager.setActiveEditor(editor); } } @@ -64,7 +64,7 @@ void disposeCompletionHandlerFor(IWorkbenchPart part) { if (editorPart != null) { ITextEditor editor = editorPart.getAdapter(ITextEditor.class); if (editor != null) { - manager.disposeCompletionHandlerFor(editor); + manager.disposeCompletionManagerFor(editor); if (editor.equals(manager.getActiveEditor())) { manager.setActiveEditor(null); } diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/EditorsManager.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/EditorsManager.java index 6a042971..235e605e 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/EditorsManager.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/EditorsManager.java @@ -12,13 +12,13 @@ import com.microsoft.copilot.eclipse.core.lsp.CopilotLanguageServerConnection; /** - * Manages the completion handlers for all available ITextEditors. + * Manages the completion managers for all available ITextEditors. */ public class EditorsManager { private CopilotLanguageServerConnection languageServer; private CompletionProvider completionProvider; - private Map editorMap; + private Map editorMap; private AtomicReference activeEditor; private IPreferenceStore preferenceStore; @@ -35,25 +35,37 @@ public EditorsManager(CopilotLanguageServerConnection languageServer, Completion } /** - * Gets the {@link com.microsoft.copilot.eclipse.ui.completion.CompletionHandler CompletionHandler} for the given + * Gets the {@link com.microsoft.copilot.eclipse.ui.completion.CompletionManager CompletionManager} for the given * ITextEditor. If it does not exist, a new one will be created. Returns null if the editor is * null. */ - public CompletionHandler getOrCreateCompletionHandlerFor(ITextEditor editor) { + public CompletionManager getOrCreateCompletionManagerFor(ITextEditor editor) { if (editor == null) { return null; } return editorMap.computeIfAbsent(editor, - edt -> new CompletionHandler(this.languageServer, this.completionProvider, edt, this.preferenceStore)); + edt -> new CompletionManager(this.languageServer, this.completionProvider, edt, this.preferenceStore)); } /** - * Gets the {@link com.microsoft.copilot.eclipse.ui.completion.CompletionHandler CompletionHandler} for the active + * Gets the {@link com.microsoft.copilot.eclipse.ui.completion.CompletionManager CompletionManager} for the given + * ITextEditor. Returns null if there is no manager for the editor. + */ + public CompletionManager getCompletionManagerFor(ITextEditor editor) { + if (editor == null) { + return null; + } + + return editorMap.get(editor); + } + + /** + * Gets the {@link com.microsoft.copilot.eclipse.ui.completion.CompletionManager CompletionManager} for the active * ITextEditor. */ @Nullable - public CompletionHandler getActiveCompletionHandler() { + public CompletionManager getActiveCompletionManager() { if (this.activeEditor.get() == null) { return null; } @@ -61,11 +73,11 @@ public CompletionHandler getActiveCompletionHandler() { } /** - * Disposes the {@link com.microsoft.copilot.eclipse.ui.completion.CompletionHandler CompletionHandler} for the given + * Disposes the {@link com.microsoft.copilot.eclipse.ui.completion.CompletionManager CompletionHandler} for the given * ITextEditor. */ - public void disposeCompletionHandlerFor(ITextEditor editor) { - CompletionHandler handler = editorMap.remove(editor); + public void disposeCompletionManagerFor(ITextEditor editor) { + CompletionManager handler = editorMap.remove(editor); if (handler != null) { handler.dispose(); } @@ -84,10 +96,10 @@ public ITextEditor getActiveEditor() { } /** - * Dispose all the handlers. + * Dispose all the managers. */ public void dispose() { - for (CompletionHandler handler : this.editorMap.values()) { + for (CompletionManager handler : this.editorMap.values()) { handler.dispose(); } this.editorMap.clear(); diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/EolGhostText.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/EolGhostText.java new file mode 100644 index 00000000..4bc2a905 --- /dev/null +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/EolGhostText.java @@ -0,0 +1,32 @@ +package com.microsoft.copilot.eclipse.ui.completion; + +import org.apache.commons.lang3.StringUtils; +import org.eclipse.swt.custom.StyledText; +import org.eclipse.swt.graphics.GC; +import org.eclipse.swt.graphics.Rectangle; + +/** + * A ghost text placed at the end of the line. For single line ghost text, we draw it by ourselves. Because the code + * mining API will put the cursor at the end of the line, which is not what we want. + */ +public class EolGhostText extends GhostText { + + /** + * Creates a new EolGhostText. + */ + protected EolGhostText(String text, int modelOffset) { + super(text, modelOffset, GhostTextType.END_OF_LINE); + } + + @Override + public void draw(StyledText styledText, int widgetOffset, GC gc) { + if (StringUtils.isNotBlank(this.text)) { + Rectangle bounds = styledText.getTextBounds(widgetOffset, widgetOffset); + int y = bounds.y; + y += bounds.height - styledText.getLineHeight(); + gc.drawString(this.text, bounds.x + bounds.width, y, true); + } + + } + +} diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/GhostText.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/GhostText.java new file mode 100644 index 00000000..a690b93a --- /dev/null +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/GhostText.java @@ -0,0 +1,19 @@ +package com.microsoft.copilot.eclipse.ui.completion; + +import org.eclipse.swt.custom.StyledText; +import org.eclipse.swt.graphics.GC; + +abstract class GhostText { + protected String text; + protected int modelOffset; + protected GhostTextType type; + + protected GhostText(String text, int modelOffset, GhostTextType type) { + super(); + this.text = text; + this.modelOffset = modelOffset; + this.type = type; + } + + public abstract void draw(StyledText styledText, int widgetOffset, GC gc); +} diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/GhostTextType.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/GhostTextType.java new file mode 100644 index 00000000..cca031d8 --- /dev/null +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/GhostTextType.java @@ -0,0 +1,16 @@ +package com.microsoft.copilot.eclipse.ui.completion; + +/** + * Type of ghost text. + */ +public enum GhostTextType { + /** + * Single line of ghost text placed in the line (not at the end). + */ + IN_LINE, + + /** + * Single line of ghost text placed at the end of the line. + */ + END_OF_LINE +} diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/RenderingManager.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/RenderingManager.java new file mode 100644 index 00000000..df1a5862 --- /dev/null +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/RenderingManager.java @@ -0,0 +1,102 @@ +package com.microsoft.copilot.eclipse.ui.completion; + +import java.util.ArrayList; +import java.util.List; + +import org.eclipse.jface.text.ITextViewer; +import org.eclipse.swt.custom.StyledText; +import org.eclipse.swt.events.PaintEvent; +import org.eclipse.swt.events.PaintListener; +import org.eclipse.swt.graphics.Color; +import org.eclipse.swt.graphics.GC; +import org.eclipse.swt.graphics.RGB; + +import com.microsoft.copilot.eclipse.ui.UiConstants; +import com.microsoft.copilot.eclipse.ui.utils.SwtUtils; +import com.microsoft.copilot.eclipse.ui.utils.UiUtils; + +/** + * A class to control completion rendering. + */ +public class RenderingManager implements PaintListener { + + private List ghostTexts; + + private ITextViewer textViewer; + private Color ghostTextColor; + + /** + * Creates a new CompletionManager. + */ + public RenderingManager(ITextViewer textViewer) { + this.ghostTexts = new ArrayList<>(); + this.textViewer = textViewer; + StyledText styledText = textViewer.getTextWidget(); + if (styledText != null) { + SwtUtils.invokeOnDisplayThread(() -> { + styledText.addPaintListener(this); + this.ghostTextColor = new Color(styledText.getDisplay(), new RGB(UiConstants.DEFAULT_GHOST_TEXT_SCALE, + UiConstants.DEFAULT_GHOST_TEXT_SCALE, UiConstants.DEFAULT_GHOST_TEXT_SCALE)); + }, styledText); + } + } + + /** + * Redraw the canvas(editor). + */ + public void redraw() { + StyledText styledText = textViewer.getTextWidget(); + if (styledText != null) { + SwtUtils.invokeOnDisplayThread(styledText::redraw, styledText); + } + } + + @Override + public void paintControl(PaintEvent e) { + StyledText styledText = textViewer.getTextWidget(); + if (styledText == null) { + return; + } + + if (this.ghostTexts == null || this.ghostTexts.isEmpty()) { + return; + } + + GC gc = e.gc; + gc.setForeground(this.ghostTextColor); + + int widgetOffset = UiUtils.modelOffset2WidgetOffset(textViewer, this.ghostTexts.get(0).modelOffset); + // will get index out of bounds if the cursor is at the end. + // Because there is no more text to get bounds at EOF. + widgetOffset = Math.max(Math.min(widgetOffset, styledText.getCharCount() - 1), 0); + for (GhostText ghostText : this.ghostTexts) { + ghostText.draw(styledText, widgetOffset, gc); + } + } + + /** + * Clear the ghost texts. + */ + public void clearGhostText() { + this.ghostTexts.clear(); + StyledText styledText = textViewer.getTextWidget(); + if (styledText != null) { + SwtUtils.invokeOnDisplayThread(styledText::redraw, styledText); + } + } + + /** + * Dispose the resources used by the completion manager. + */ + public void dispose() { + if (this.ghostTextColor != null) { + this.ghostTextColor.dispose(); + this.ghostTextColor = null; + } + } + + public void setGhostTexts(List ghostTexts) { + this.ghostTexts = ghostTexts; + } + +} diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/codemining/BlockGhostText.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/codemining/BlockGhostText.java new file mode 100644 index 00000000..9e1271bc --- /dev/null +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/codemining/BlockGhostText.java @@ -0,0 +1,23 @@ +package com.microsoft.copilot.eclipse.ui.completion.codemining; + +import org.eclipse.jface.text.BadLocationException; +import org.eclipse.jface.text.IDocument; +import org.eclipse.jface.text.codemining.ICodeMiningProvider; +import org.eclipse.jface.text.codemining.LineHeaderCodeMining; +import org.eclipse.jface.text.source.inlined.Positions; + +/** + * A block of ghost text with multiple lines placed in new lines. We use code mining API to display the ghost text. + */ +public class BlockGhostText extends LineHeaderCodeMining { + + /** + * Creates a new BlockGhostText. + */ + public BlockGhostText(int beforeLineNumber, IDocument document, ICodeMiningProvider provider, String text) + throws BadLocationException { + super(Positions.of(beforeLineNumber, document, false), provider, null); + this.setLabel(text); + } + +} diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/codemining/GhostTextProvider.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/codemining/GhostTextProvider.java new file mode 100644 index 00000000..14885e3d --- /dev/null +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/completion/codemining/GhostTextProvider.java @@ -0,0 +1,52 @@ +package com.microsoft.copilot.eclipse.ui.completion.codemining; + +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jface.text.ITextViewer; +import org.eclipse.jface.text.codemining.AbstractCodeMiningProvider; +import org.eclipse.jface.text.codemining.ICodeMining; +import org.eclipse.ui.texteditor.ITextEditor; + +import com.microsoft.copilot.eclipse.ui.CopilotUi; +import com.microsoft.copilot.eclipse.ui.completion.CompletionManager; +import com.microsoft.copilot.eclipse.ui.completion.EditorsManager; + +/** + * A provider for ghost text. + */ +public class GhostTextProvider extends AbstractCodeMiningProvider { + + @Override + public CompletableFuture> provideCodeMinings(ITextViewer viewer, + IProgressMonitor monitor) { + return CompletableFuture.completedFuture(getCodeMinings()); + } + + @Nullable + private List getCodeMinings() { + ITextEditor belongingEditor = this.getAdapter(ITextEditor.class); + if (belongingEditor == null) { + return Collections.emptyList(); + } + CopilotUi copilotUi = CopilotUi.getPlugin(); + if (copilotUi == null) { + return Collections.emptyList(); + } + EditorsManager editorsManager = copilotUi.getEditorsManager(); + if (editorsManager == null) { + return Collections.emptyList(); + } + CompletionManager manager = editorsManager.getCompletionManagerFor(belongingEditor); + + if (manager == null) { + return Collections.emptyList(); + } + + return manager.getCodeMinings(); + } + +} diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/AcceptFullSuggestionHandler.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/AcceptFullSuggestionHandler.java index ed356dd9..5b9c118f 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/AcceptFullSuggestionHandler.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/AcceptFullSuggestionHandler.java @@ -6,7 +6,7 @@ import com.microsoft.copilot.eclipse.core.completion.CompletionCollection; import com.microsoft.copilot.eclipse.core.lsp.protocol.CompletionItem; import com.microsoft.copilot.eclipse.core.lsp.protocol.NotifyAcceptedParams; -import com.microsoft.copilot.eclipse.ui.completion.CompletionHandler; +import com.microsoft.copilot.eclipse.ui.completion.CompletionManager; /** * Handler for accepting the full suggestion. @@ -15,7 +15,7 @@ public class AcceptFullSuggestionHandler extends CopilotHandler { @Override public Object execute(ExecutionEvent event) throws ExecutionException { - CompletionHandler handler = getActiveCompletionHandler(); + CompletionManager handler = getActiveCompletionManager(); if (handler != null) { notifyAccepted(handler.getCompletions()); handler.acceptFullSuggestion(); @@ -25,9 +25,9 @@ public Object execute(ExecutionEvent event) throws ExecutionException { @Override public boolean isEnabled() { - CompletionHandler handler = getActiveCompletionHandler(); - if (handler != null) { - return handler.hasCompletion(); + CompletionManager manager = getActiveCompletionManager(); + if (manager != null) { + return manager.hasCompletion(); } return false; } diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/CopilotHandler.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/CopilotHandler.java index be347e3b..5c057566 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/CopilotHandler.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/CopilotHandler.java @@ -7,7 +7,7 @@ import com.microsoft.copilot.eclipse.core.lsp.CopilotLanguageServerConnection; import com.microsoft.copilot.eclipse.ui.CopilotStatusManager; import com.microsoft.copilot.eclipse.ui.CopilotUi; -import com.microsoft.copilot.eclipse.ui.completion.CompletionHandler; +import com.microsoft.copilot.eclipse.ui.completion.CompletionManager; import com.microsoft.copilot.eclipse.ui.completion.EditorsManager; /** @@ -15,10 +15,10 @@ */ public abstract class CopilotHandler extends AbstractHandler { /** - * Gets the active {@link CompletionHandler} for the current editor. + * Gets the active {@link CompletionManager} for the current editor. */ @Nullable - public CompletionHandler getActiveCompletionHandler() { + public CompletionManager getActiveCompletionManager() { CopilotUi copilotUi = CopilotUi.getPlugin(); if (copilotUi == null) { return null; @@ -27,7 +27,7 @@ public CompletionHandler getActiveCompletionHandler() { if (manager == null) { return null; } - return manager.getActiveCompletionHandler(); + return manager.getActiveCompletionManager(); } public CopilotLanguageServerConnection getLanguageServerConnection() { diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/DiscardSuggestionHandler.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/DiscardSuggestionHandler.java index 2eba32cf..9c8a650f 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/DiscardSuggestionHandler.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/DiscardSuggestionHandler.java @@ -7,7 +7,7 @@ import com.microsoft.copilot.eclipse.core.completion.CompletionCollection; import com.microsoft.copilot.eclipse.core.lsp.protocol.NotifyRejectedParams; -import com.microsoft.copilot.eclipse.ui.completion.CompletionHandler; +import com.microsoft.copilot.eclipse.ui.completion.CompletionManager; /** * Handler for clearing the completion ghost text. @@ -15,7 +15,7 @@ public class DiscardSuggestionHandler extends CopilotHandler { @Override public Object execute(ExecutionEvent event) throws ExecutionException { - CompletionHandler handler = getActiveCompletionHandler(); + CompletionManager handler = getActiveCompletionManager(); if (handler != null) { notifyRejected(handler.getCompletions()); handler.clearCompletionRendering(); @@ -25,9 +25,9 @@ public Object execute(ExecutionEvent event) throws ExecutionException { @Override public boolean isEnabled() { - CompletionHandler handler = getActiveCompletionHandler(); - if (handler != null) { - return handler.hasCompletion(); + CompletionManager manager = getActiveCompletionManager(); + if (manager != null) { + return manager.hasCompletion(); } return false; } diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/TriggerInlineSuggestionHandler.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/TriggerInlineSuggestionHandler.java index 1315f87a..ae2cd192 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/TriggerInlineSuggestionHandler.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/TriggerInlineSuggestionHandler.java @@ -3,7 +3,7 @@ import org.eclipse.core.commands.ExecutionEvent; import org.eclipse.core.commands.ExecutionException; -import com.microsoft.copilot.eclipse.ui.completion.CompletionHandler; +import com.microsoft.copilot.eclipse.ui.completion.CompletionManager; /** * Handler for triggering the inline suggestion. @@ -12,18 +12,18 @@ public class TriggerInlineSuggestionHandler extends CopilotHandler { @Override public Object execute(ExecutionEvent event) throws ExecutionException { - CompletionHandler handler = getActiveCompletionHandler(); - if (handler != null) { - handler.triggerCompletion(); + CompletionManager manager = getActiveCompletionManager(); + if (manager != null) { + manager.triggerCompletion(); } return null; } @Override public boolean isEnabled() { - CompletionHandler handler = getActiveCompletionHandler(); - if (handler != null) { - return !handler.hasCompletion(); + CompletionManager manager = getActiveCompletionManager(); + if (manager != null) { + return !manager.hasCompletion(); } return false; } From 824873e89437699026b50d98744af18d6d452996 Mon Sep 17 00:00:00 2001 From: Sheng Chen Date: Mon, 6 Jan 2025 11:04:25 +0800 Subject: [PATCH 052/690] fix - Reduce the spin interval to 100ms (#94) --- .../copilot/eclipse/ui/handlers/ShowStatusBarMenuHandler.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/ShowStatusBarMenuHandler.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/ShowStatusBarMenuHandler.java index d0a706ad..c5e939ec 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/ShowStatusBarMenuHandler.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/ShowStatusBarMenuHandler.java @@ -202,7 +202,7 @@ public static void createMenuAction(MenuManager menuManager, String text, IHandl private class SpinnerJob extends Job { private static final int INITIAL_ICON_INDEX = 1; private static final int TOTAL_SPINNER_ICONS = 8; - private static final long COMPLETION_IN_PROGRESS_SPINNER_ROTATE_RATE_MILLIS = 200L; + private static final long COMPLETION_IN_PROGRESS_SPINNER_ROTATE_RATE_MILLIS = 100L; private int currentIconIndex = INITIAL_ICON_INDEX; private UIElement uiElement; From bc1107553dab67f443ba04b69ab56f1415f940c9 Mon Sep 17 00:00:00 2001 From: ethanyhou <149548697+ethanyhou@users.noreply.github.com> Date: Mon, 6 Jan 2025 15:17:49 +0800 Subject: [PATCH 053/690] Added readme for the repo. (#95) --- README.md | 49 +++++++++++++++++++++++++++++ docs/adoBuildArtifacts.png | Bin 0 -> 35416 bytes docs/adoDownloadZip.png | Bin 0 -> 207071 bytes docs/eclipseFinish.png | Bin 0 -> 206083 bytes docs/eclipseInstallNewSoftware.png | Bin 0 -> 386249 bytes docs/eclipseInstallNext.png | Bin 0 -> 291972 bytes docs/eclipseSelectZip.png | Bin 0 -> 404596 bytes docs/githubCopilotIconMenu.png | Bin 0 -> 20139 bytes 8 files changed, 49 insertions(+) create mode 100644 README.md create mode 100644 docs/adoBuildArtifacts.png create mode 100644 docs/adoDownloadZip.png create mode 100644 docs/eclipseFinish.png create mode 100644 docs/eclipseInstallNewSoftware.png create mode 100644 docs/eclipseInstallNext.png create mode 100644 docs/eclipseSelectZip.png create mode 100644 docs/githubCopilotIconMenu.png diff --git a/README.md b/README.md new file mode 100644 index 00000000..121f9d3a --- /dev/null +++ b/README.md @@ -0,0 +1,49 @@ +# GitHub Copilot for Eclipse +GitHub Copilot for Eclipse is a plugin that brings the power of [GitHub Copilot](https://github.com/features/copilot) to Eclipse. It provides AI-powered code completions and suggestions for Java, Python, and other languages. + +## Prerequisites +- [Eclipse IDE](https://www.eclipse.org/downloads/) +- [Java 17](https://www.oracle.com/java/technologies/javase/jdk17-archive-downloads.html) or above +- An active [GitHub Copilot subscription](https://github.com/features/copilot) + +## Getting Started +1. Find the latest release of the plugin from the [nightly build pipeline on Azure DevOps](https://mseng.visualstudio.com/VSJava/_build?definitionId=19562&_a=summary) + +2. Open the latest relase pipeline and select artifacts under the `Build` job: +

+ alt text +

+ +3. Select `GitHubCopilotForEclipse.zip` and download it: +

+ alt text +

+ +4. Open Eclipse and go to `Help` -> `Install New Software...`: +

+ alt text +

+ +5. Click `Add...` -> `Archive` and select the downloaded zip file: +

+ alt text +

+ +6. Select the `GitHub Copilot` plugin and deselect `Contact all update sites during install to find required software`: +

+ alt text +

+ +7. Click `Next` and finish the installation process: +

+ alt text +

+ +8. Restart Eclipse, and the GitHub Copilot plugin is located on the bottom right corner. You are ready to use GitHub Copilot for Eclipse! +

+ alt text +

+ +## Reporting Issues +Please report any issues or feedback on the [GitHub Copilot for Eclipse GitHub repository issues](https://github.com/microsoft/copilot-eclipse/issues/new?template=bug_report.md). + diff --git a/docs/adoBuildArtifacts.png b/docs/adoBuildArtifacts.png new file mode 100644 index 0000000000000000000000000000000000000000..9ea85e9b006d213452caa0af5a803642ebbc88c9 GIT binary patch literal 35416 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ÐnpHa2g5k5^y#H~)U(m;CmZkMj7lAM$FM7PC`CtrQT1 z6J_XqzR18qiR1}Ds|zK5lp2Hnlox!CAb>)N;)JqBmJv>5EdDxkfNY_!y_KrsVhZ+r z$fqB@%xf?HgFpY~e|h|;zvhX*eaIoVn?8*Sr69nG_6@zy88f&hh#N!X?3pAhwS;@z zSR7{ZbIWOIKAA9Eu=Uqdyt|L?Mmq%fV^5t-N>n&03Xq3%yg2$YMrTiBc~BpIqJ*`- z4RhuDy#B;sv~lp-Tw4pZUG24M?X1eo$c%{hd1XxI6EE7`o^;gAy|+et zlG?Iow}n~<#ma;PQo%k0L2eP?Ke2&=Li&{@kVa0){+gst1TM_{T z^I{C_Oa2(KpuD_jA=LPKBI6;WyIoFsbikw!8sJF&b7(1(%^h)h2_=K+Ul?sZ5G)y; z=Z5u=rZc=`ZD)9O)$07QzNFJ`fQ+@*BXaKTh!wT^aGYhcp)T2u^*e3a?h=(VzJ#57 z{danYD5#W@(Xz>V2Ay48w?lIy|j+2)bv2em< zJ!hq+-v^+iYDc*+Yjpu%AHG0JK0WVPQBMA6^kr zfg0l58*mWLA|hFf#>5$?}!{;wVsl-m*!QPY;2rr-ENIax(ZUYC4}+%*B{o3ITlPB4xX4F_K)Vx zhm^z8V6ZUYm9fI|ys|O$cP8J_@e*kZqOyetx<1~P>%P! z8ebSBLC)%Wq4d>LNvSZKsRP3uu)ZMO#TpI=Y%fGpf9KJ0Y$b--CwEC+1fAkYloJ{t z_MJH`tj~-3_qtk48OFya-16@NrZG~n@{pu?k~-Rl(ufBe?u_wDrfg#%yA&?zx?@}`>$V}cd85PEw`Or)%A4aL0hjm!Hz z0V?C4&Rws9@Y>JG%0AUGdTF3uyx2kba06VMZJcc+cQBg`ZEm6riewTbLBpFb5wWkYKlEa zRNqez8*SECF`FMh--Era$;yVuJZI3C|D9=_IeNU_QsbG@?8#0D{CD4UN7q(XgM^Xm zLu-usn>lU=c_21NANM8X9O&WXXu;*v74zgl4R=v+6hL7?%|DkDOgRm=eFv(2ZcT1* zm^{Az-4L@lFjEmj?h9N7A?eXhFgN_3_0PkK^;Zr+7X=MLE+Hg@CLCj?Ps2G&#%>G< zi+yy@%bvMmGH@o#S&^N@@}~$4FTW*XWX=(tD2q?V4H!HU8WQ!DB_S(eidM_KMDkiB3MMbr zv8gg3_1Fv>``q%Baekd)Um+BQmplT>8QtM6^P}zUd$j%H{7om}x=);L5BKiGwdJ@e zfk@h}cpt;_<&47P)xGv%yWco@g>7MZQvjd@qtm^5^dvp(6Cp++XYzbEQriYJGW>kX z*|ncBlT(X)>Ri`STTVj*Tj>(6J($JEX9rI##SJ9M%R0k&Ht%BJ&&C>~B_HCC(6>8w zDBnF`jZV_SA%zA^e_EwMLlzQL)eL{V|A5^sy%gX;LiiwSN)8gSiyDl#~ zSmkr;{p87gePej*x8F9hFBAEh-S9;L1=L;&WwnN?UHOkVhMQyJBqoj{RS+iV*H!tH3TR0jh&bK&?y~q}J%Pa3rwZ4MCi}dnyzzudb>k z&WtPjPx_dQc1x(W`jnBU8DWnwL#If>QUBsxP$X#QM2z#P%r_meBr<&&n&dqpHEKB< zw#eOYUnl)pZXf5b!QDb}e9m>ew=X$b0eKtX(C;r+*yS_`b?qGw<0CgB9vcl)tTI?B z#i89Q0!T4mC2+qXte@u@%r-zAb>s@^*MOx46%5m&E98{lziU=}M`5$Yohurc^D+Sq z$^3RB*?N-ICMxHu2o`Hv#7`6{g7s()Whtt%4L_oXc>|aNok) zY`E6?+i68smBwK9X3CP0fhI2W2Ub^A;unYVOPJ$ zk>3&MaRD;Hv@fRS0aG<=dp~Gp=eUM0j7#Z64049pdRi$}&y%gHwmsDPZ+oisU)@31 z72Q!+j?F$e>y>)DEdaJxlYOC3*8=sPqp_4%-Q(S%1*6V;LA&dFgvaK!DLs*%cfCzg zMzbwDrRwitCPS0`ROUt7_d8CT-xc@rR#Rk3R09@Wwl|ABcTVwyDoke3BzZ5d-NTK|C%8e4APkflk8J5)+PphY zlTF`E6{?a$LZ=KI9;aX8$vz><)D;|nknh8KXoQ_bqzkTg&VD4_Om>k}s+0nJ&3 z@xkz(2#e8!P6sL-6t4b@MO@>D?e$n^Qw zz|c{=RPA#q>G$$U)Ad=Fch~5;kdcBNtW_dv4N0zIDy02_^yBpjmg`;g#|xAF-*w5R zd|6pmXjgaF?JLhA$BQ8X?8pp7Vlr2&!<T^p3t?(2AZU}!Zg(v+2djFKNNd>bM=y_*?2-$L9Aki_9!0z5BpsIH&?yy znW*1iC;WG1`aW?4em~%UJH3z|8Wtp>U@6A+gB<=E*G2%iLt59N z(sh1jp{5{jkV{KB$;sC7n~0nwQHl@bC^tyr_??Psgkdvgb1&cje!f3U^}bJP-=?~0 zG=9^g^QU0v$o~@bqi5|F*ZBGI8}--Uo-%}4o$>lGjc0p}(;gcS^B?Wi_sb~zd3MI{ zw4{I7X9LA=$dc*J=v!>VF@Bj$qUAN*j8S;l ziIa6=dpSx0r_QQQ_Ily*_qcPfCGhP~y)c2xD+&t*LP5d}kV$e*RMoS$GAG7ko&Z-( z$%rN-=_SE*>yfg~5;S`aNryIuX~7Zgfm*nNBACIQou3tKJhCB2O(1jfoZZ}%Xreko zfsFrgjwyd4V*L#tA{G`LOe?wQ>NaQe366jBWmY{xkDCaQb}3kc91ayXqV7+9l=JH! zWFU^F+re}8*;x)~lkp|HqAt;XlO&`Tzlbkufftz{#>OZ$WWG|70wqLHLeOqPJTLxl z{k28Oe+|K-BqWj`Uz~M!N#dJf4X~>=oVc+-97!n0#wT!eg@_Uk@sWRiVU3XxwX_(s zJ}vMS=PHutQlBoaAPyJ54=`Aj45VQlkMfZYfd$LInauk>Q2ml+fkL&?@6V?iWb@ z3_HYIDQLKB9VzI_3}#sjen6VlUXMt2oxV`gEMiFKWq3AO&?!cBzLEXUP}BhS4YeR< zof@Ayq!c2u1XfmGHW#=GH`#S1_B+f2rEA9-SegHrypMOfZw=I?Dcqsqeoupm&k$|Sa?;s4<=Ay*)wC2Au zv7$2+El;nt#wkxvjxYfr1%=a6-#H?Ygd6LB>0(yfmCIIj(m+gt_=7YflLjDC?dBy5 ziF{)E;L$I_K~&I;nW`5hB?P1JRi#Un({N2bA0XF|g9!5V7n8jk&*p^5%asKDQw+9( z6Y&6Nj?lEyW?W)7rej(6u~>UGuu(rj;sWAU;8NxEe>k0P-Zc4f_OEyZmZL zr=`7Jy8x;%G+}Z+AxMH^Zh(#RGC1yt@nSlt*Nllj1&9+&_tq2YB2@a-KnM*1wLkEF z!x%u5RAtwZ+pI2Qq-Dh^1?qze$I78zPg0BWV2-1M1W8QT7k7$@Al3ZGz_QkzVj!}wkRaIAvr6m2M+aW|Rv1l;5cxw9^uO7ork08#XQ6s5pzQ={OVNs{ zj*utJznUR2A?4*rt@Vh(W58YI*`AMmo>w$2-GQF*JK%9kdrBhQatP>qX-8sW4YeMz zK;>}Z1>^XLLz<{y+V)49Hyyrgj!lLTX~!^Ke4AJo6S9<}#*VpPBKb@B1L`wOrnJZQ zFIJqs<}D|&Y6fOjAlhreR_^c)m$EE5S*}|C+pQg@w^q-OaCe8+l;yVlzxys~{UUCn@Em05~dw@6(xh1Kf6JoSGcOO9THTI zTsaX@9?YJPGP5g^uKHViF%Sa-Q=7txDU*dc<6(Y;E^~n;;$V+k+FI_GYX!)rDvF00 zrl$)U@IVkKgELc!{u5UVY=dsWD<)m* zIaC&l-x?CxZ~#z36e>{ChsX31iXrn#iCeS*-{vc%%>%^9r%ZJ@5hdMa1X?f3+p9xJ zNmc?(E|18HN=4l!Ab}FfiIu`vxW(NFjVy=el_nfb-bL=RYn?+*IR}`_ zkV=-2%56sWD|h?)>9xEC8(i!7%d($C;a&WID3#3PF|nk^^WC4;h=LbgR~S$3Elt{Z zEkM}{>oMwkV(nfWfHr4{zFDw%jEP=oA4r%5BC_yJT=AZ!dD)G;y*Eu`-~R%ds5H!xFAH(ssY>eB?o;2oA{5ae8HCeqpU$QO*Qk^@#D_R&!D_^imnn}E(V+-c3==;8Oqn_^t*%5n zVi;r`N%zr~2*}Nb9iy8!sQzh_m@|b^RTu#=w5*IV?S8A%;=#j#vM0p7QnH`Ikp96K zjTEfo?50)8sW`A@#3{%MlUOnJpL&U-3}>`U2T|h;aFQ6r z0bEI6+!B1xbf(bJ!K=h)tjaaLLGWpemg(f5yni!H+C+71ya=tGZ}RdV8< zn1?-GI=KN!HT5JM`6CqKBrhg*kJ;4)`>24THrXIkW?Lz05Q=vq4cwTX>?dXtJxP907$SV*jG3e=Slc z;cn{Y>*-=JC`5sZ-`ce>UarXEt7kE$N-(wQB|D33*tYrTGl&h(w)eYv`^Q? z28i=RNYJlS1?%KfIDEi;#KL~qlD4c+b-+bRGz3rF7v8(&+W6ob{QWm`!@jmhSs_8k zE-QT&?r^#Wh*}iN!kkfNB!YsXx|Cj;NEKVKKrgA;j4UC$jlq#a<<64xCE_SFywa-4 zsM9z*OPyV#`|Qq&a2erl0`EUQFG#3a*dC#d` zaGpeh2o!?y4kw{K?Y~0~I(@upQd%uDVw_+qUfVnMnE%~O7obAiwNyD68}7eoA~w|) zCQgt&s+tspIj8m2DWt&MpZ1eYu+0dwrvz|0#x!u7w z&Ea|L2=2;AS2sU_z1{CrH^**JKzU*V9UY9jT)V=RZA>kXdsJ3Kmd;0%@wvHC+kE`rHzAYLj;S9(>Fj4LPJL zvY0UYe)_0ceAXZo;6ZGA7hhSIW{YFHunpjOdmXX5)-7ZiE<<>32_V)vUr<*-58Q$Oi z7rw_I&3uNnMD z3o$o$VQKKr)f0Z>e&-ca2eZyUgH<2rt7`-yf;9Q6QYQs@)3}0&kkL|vl*I6gsftxL ztn7<*rVEBvnO+H`XEa|llE_N-T(08F4=3jEjRy)CJ$xX`$rzER2#gM#Xt>AXLY2TN zVj%;Hg8rjw>^Ig0ZJwNOSRQ39%SzPSeu3((X}5Ys1jNADcN?1S7R^A>dHlI;hl-GD zFq!W~Dpxax_I6AuoQziFh^VYtZyCf1B#kIe@AO?OH(l5V|0oC&U4uR6qWG4ZuLxH z({Iy4QaPYl(I^qMf`Y>Qc>G_#N4nzYr8&R1Pq8ERVSk3%Y2V(RheFj7Cam7l-P*8> z_&2%R_e1Tcxi{7aw(P1Y+H;+%j^C7)pTi7o>Bzg`ZRHC=z&v0Ss4i6vPaO3e_B32|=V)5IsT z0k5=ecI;6G(LFBfFAatk61JA0FYDeLymj0+zb4gMpSwQ5d%1B7J@a{DtLMuTmT1SJ z=w5AX z;R6FYvB%dY8)5bj%T=|v_Z!-)*~;Vfax`jj!R^{Zi0rX6es`4W^isrA_6B?JDLpEc zh{CWgc=SUmnxa_)5Sr(M%bK!l1;3@vy)8I+6jr?%u{tgOa!TwitH0f_)IN-Bi&I@O zGrFs?_ZxbhTk{Aw6-4I9s2t;_l3U*`!1@-#u3AoQ$-X7{NZC+NE-MhDC}sF=p&Bho zg~oVVQ5W`f1-rvHA2e;uij^WJH>ZnM_=&v1`-1<07tT*?gM@es!?v`%5T2*JeOz2{ zYWfrOAw|iu!K6($Yh7G4D53o12kl;S@>G^<{mhQ|29iC3SW_`78a)>*TOguT<W<_e@yXeoK$(l%}#}GIm?9=k{@+8gbqVYOTAjq#b#$=%L1e)$#>@HMF!W_1+;` zH$h|9F|aM|Aj~T=jahv3K)meMc$aBfRCTbq@!Iw~W6S-yfpbJ=x}f0|v<5ITCcMa|G@+5nleKFDY{!>(Wo0EMlAH~62(8)>D2 z<+Jc8*IA_%$+tOn9WOFFYl?0*@v)5@dBdm#wN8|fH3@ll2D&Fxm1BU%2FCYI$bO%7UM#lGfBvYTSIq#d^X+3 zYMOU;r5=o%G(ig=3SP}sI=59hXHy7WE&s&B4Z-fhKKuI>bN8|hxqeIZhFz{mLv3Fg z99aq_o0pLo*O}c}A!jdEnq0)FVR3Iya3Js7A&1NCz5{Wl>?4-9rCs@S(P@Ux)+x&uVgl_v^ z>(NItSU59><$K#_nt*1b&nuVKCTAC8Ds;smI_=x0WCax!IZI=NuR!3eimdNx@XX4G zvA11*Kv9O~cV|I-_Bm@Iva8I$)531){GI9ne%jRPBWcN~MHFrsl=tygMbAgX5A7z6 zYO4X{M66a9Bul|aB(407yEdFLz?Ku4iO&55p4}lcB9A@MMWuM!!wNw;+m3@cnbPIH zD3vxGq*k|}=^32ZU?qi9BT)#jq9fRfX>L-xG#L%W@H`0lxnFM)auOchN68v`Y(dYa z=h-@ER0%lH@bHAjD*6oN!hM?6Ru>tLy~4dpIc);lzsUMGc~P2xycf4Kdp<}cg=kc8 zEdlcMVO4+OWy#X)`00VeD@M~Y=ZwYc&-5}AH=yt6wIXMHjdMsaW~eNO1>x>9{v9-| z!kTsFsKJ=!&D7j7x1}pDyUp&z-Zhz2dt;z%$IGjR;LDp3-D|_Ovc(riRY2V&46pY; zg^UuHBoztAiNJhb<~-tY$8{$H@Y)Zt3yY?sRxO5XuCq&~g^l%rp@s4uC&MQm2V3%M zid*vH@(j!2P_Hw+K@e#aN$B}}@~O9g;KPLh0I_1Q($g+MRIJ`(+owJ&*}{-<`eC!w zwk2Gs&lrfB5xy@>iR_rN`e6ndW2IyZ2zg*46(L7R%2CkJL^*0|n&ccMW@HGeJ(G^e z=Zf=C%0VKR_2_lql?JMlJTd?cK`Xg+580ahK*BIHX12{ zBj<8wU`?pidu^#sO5Tyka77b?gi?k|0i@w|>&RF+OsORt#za#wxA+o zK~=3O*QLfP1TtR;FjUnR#qAr>XmE0_ppM4oO#V-3 z)$N{4YY8B0OQBcNieT@AF3aqvmStUY!CSV&M%Se^m6>M747zx9F3V}kdRjvLd)>I3 z-7zs)&=q&2$lh;Do1LV;O1iVU-(D%x$SiE1Pl+4-car-|X#f%fCGyY~h|g_9t}(qFLJm5ZQ1*TVbopRRy*B zq8giE`Irq8GpR zh}~}}Y_@&k2bY-nSpJ^8A4$qYHq{*{@E-HAtk1r}2r!WnRz6}H0V{G>bIRg1xj!4T zxEo&E6`X5;)O3q+^JJZI>d9$ZVuidxjPbrw<=L%|d^4j=5jCc}PWe{no=TU#8OJWR zS8!M{V)pnBD}2SuqF&CBl~u+bjAEDD+m(A4xHr0=z`Lcj zD6dor<0Q)+cUf!tF43mOA`BA3vuR@>qG|a6-ly}5vvcF@^n7%FMb=*)hi&xS@{DPI zZAnVpl?$!J6crtp_+}TNdU;iLiqxuj$Qn;JG2POsiddlz6zNe_)KVR1E|}3!gH?Tjs3|D`o#2T}9TqBo&bX#Bzm+)-drh=zOe$_J zKu8IR+nsD7b>^TF6mJ~q_1g8MN6yan36y3W58;V4eli{j6 zpRYEjj=R9mJ4{%C?nq^IhtI9i>zw3h3nFXHf_O=W<2wUN=Y9CR7{bQi4{)oj^E=u> zl+7uw;Mi2u(UrB7q#XBOux|j#)NPBc=q9GH8tke)vxcFMzrng{?g}62TS9YpPnLaB=s9AcmJd{F!xg@Em4m=9(RuuWV%&N*(I*m?bQk z!4taj{ez=#~v!$sLq+g5i#?nXj+q_RH+pbID!2d5jr22@;EWTfzN`IHGvMuh|)+t zmu|Y5HP03ZtBxY$5f-^QFKTIntI~=pS1wf5IJ2)T>?n@k4E%i$eNRbURaUwbpj5Do3Y%PR4phOGH*ybRL)$WE4Uw|j zKerHD-~Co~KQ}KWEzTeo6`a3Vv~V_Ose0<-1v^HQ?teA)`?=^yCVCcbXD5&77q!nI zaD@D0SxX&qQ9VJHR-9bfuQDWMUn(+Du*0JxMFk}}(Wn&sG6cl~v-(@{-o0a54n;f& zPhWeOZZnwmcut?jR3?75@VP;^{H*Ozlt>~9+m>H0>mFv+8Dds4v$v5%(jYAzI@^<$ z#q~uPIr@klfTNmpl%dd}!;C>+ju?{YCniYIVD{o6+uRB0n0T1p#fYg*al22S5(5W+ z+CZWd_4)hR1Ehu6jLGBY%>)07?nuKadge>d;59bFR5!MdkC23ZLwddO0Or%u)*V-$ z&_X}2it3Ug1$g0H!1z{7eNSTKzR>;s1Ngdvf|RE&g)TfBb_}xZtcDQt-~E358q=pu z&H*{>2?0>1>~v6nB_o~1V~%;_l-t+SS!>=l?@Y@IPN_Wuel%1-l-md!qsouX`&Z6| za2w^F=HS|p@XTwUsC<>bTr5}yMQS@oJW?u(P>b2XzGw6AD2Dfmwdtkc;YmEJCwk2I zARU>_1;-B_5wtJvmp{S$z84kmGQd_FuS73Q@2t<%CclLE(N}VllwPE`{ng)vloA9_lfpLkG-{SCbJs zT?wmvr{G8;96NOq@$GeKQCAo|uRNgP*&)_r*%&d7N4$#x#juft)hy|}F%i*PU|AGATZDNL$nkHkFJyS4?bZ2P1^LPy*y3WlF@er zMcA=FS-I=PPX4hq1I8+q$62-rF-^qR~xQ3T<(tl%%G^6+aAsk@vk8igAdsr4VaC=7` z0?2zD-U9Y)2N7Mw(ABY__fh7L22I)yJNhJJSWGbo?!DoO+w$4aA(Sk>2rd+4$=iCu zr>2F)^_Vf6f*q@2yH;cID01$g#r7443*Q&V3pg0fWNewX(ws_PuFLP;U!#|D?*M30 z?pO-V0;VhSu!8;BXfpol1jVRsx-D|Yu1ABKUjHg1TmPW012!noY#1_b+VCq@V>&B$ zI$}x+zA_KHGJ~%fjISCFdlp=k?mz@k?`gfsr1o&7YwN?8wpyc?9(je~I+DhguPT#C zb0|_;?(k*RF|(Trx8~ZxzsMI+W7~qp=8zFP9JMU4tgVQxBB5IX+51O;LH&_lkNbtw z-^`@R(_c?-iqK3JOpEPt0{~~ao=*JE!GI0JQ8S!VmaHv4LJ*3`sn{*M;r&#Hs;|2k z2@>mXwrU-B4212DWK-o)#G5MY_6Dt^IQv7)hBDJ6^s3<%+rpamn7{Ze-Ct^#>^xs; zP(*|ot5oXF7s5LS%gt%dZx;H2Uha+l{(LvrRioD~MB>pTv=_{3rB-)8kwy~WHmp}0 zLj8XSaapU-WlcH5)ewwKCukqJ&k)C{4c?;Z4^A>g7o(LLz~X zw^MOcf`5h&ju>j45P9))CN(W}IN)LkP(EO%fL@2sqG!%hNzOx;NEw4= z?9G#tBNlB2_%qR>mSVKgqzvAf)waDj+;_b3XefpdX$VS+T=-{vlw{?5@gxZ%*`3i~ z=7lBjTR?ap7mt-c_d0Pc>KE+hb`y*;+U>}Vjf0?Ywea$L!7uJ`zaPe!GP$QbP_c&| zRb}Mkc)DF9$Ps)0)tgLZ3oT>v2ZrEtYiXUaKF=-iGd^qCAmh7o3f{3~bUxeJwI85g zYo&^sMWeUZ1g~!~)VEK{Nyi0?{IaA;P0B^)>l!$yF6!rJPi;#UDTE``n0myHd3<^{ zYFV#?ohJCQL;Nz1knGInQr{X{&#FDywl-(xM_u4Z$uyZ+15=I+UUaPUMKt-5&S6HsHScHTku8(&{3 zjh2LjBs6&MeQf?uqZz}D?cezpm z?8Y2dBYN+2>^5pJ=xNAv{ISKJ)@|p$~{XsrTM+4{aw@^5PZ7CC2IMt-PO$?BIk)x{8(=ghU<+df;BnPN0 z5_!qffmlqZVbr0&6io>UiL#=rVMpSWj4y0P7S}@RTL~JcLQ8f6g_jGVrwsP@U3I)C zayH$UNXcXG4Xl5UKcDawH0_9`9Y#$s#tui#V`306MjQPLsy}BGCFq&yXNS;L!wYwu z)Z$25EdR5I);_gup7DLtjWs<+>1mmG%HSqoWq%VNQl=k zD4{P;kqTIX=o{u2^ijGZw-Kbwp45$LHAkUY%Z@8CQ5i}2qRD!=Bg|I6-2E}=ymO6D z6iLWx@GsrqBAn?-oVBpW&WR$4<_LE3IU4}JBk+ibIlhmN>1WqxMYTJDTXv&XSEFa> zimzfQY^LyywK@|q96O_QG-iwxY@q&Avm6)yHDCi3^!DoWA!#_3@0*|Y%7gZP7{yD* zjRphDaec8pH z*zFC2pm)&WvyLzP)=3qt5HP(R=3vZtZ30xh69k_#F8e|SnNt)Ky#I8!t1h-=F?E88 z2#O`9l>3lCN@k8+dMc+UmJ>L@upZ4P=M5$itQcON4O393^kH#0PZc{bhh7PAj2u9W z`Frzn;lX3m6<|JE0{rrrP{hNSsy$xt29@fQdhLJ9sU{%_D{%5H*qe?OlOB)2y+rTD z*K-gQkiI~2fi<&c;I8%%eqalO5*5vl6uzm=6j7k_-I_0mP&`^`J*b^ z>x>>&Ta0AetavOZB-F^VqT-YHwq=fo!pnTYU(O47SOLYWU_S;Fg-7CuzX!`SC$2Ymp#UzGm|VCQ4IUSGr7bWd$n6jpypIP7feKm`PHPY!N`>Y24&8ypG}v|nRP$5 z(H!-qiU$}=8yS)bl;&O#J!k41iW!rs6E?~Kgv#G9Q*Fxa?f`_iD$AQWM0V;7tk%tc zL!A1BVU|KK2~USN8{?zRUhnU$B5_k@%P|~`OgdZxAUH5~Cyp)rPe`6*&8#2q#xEJu z*%RcBO9LW4p^C7+u~_CyL@hS_BAU7Yn^b%qNQuN~p@T4a4`wW7Ge#<&jAVgdBzUUg ztM4;Y$yrk_m|_J`S;J2&at7x=<~M&24mc4$ZkVF(e z`aq9|GI@Q9jKt8EI{YGdC((_$^nX+DYO2p*gX7Z?<36C*n|yR8XI|c7zzp2xhE{8I z|I%8v-|m0TWY6Rl4mNhQ2@NpKSjE#zoQE01VaZf99$|8{;A^M2#3mu82nr-ZI{V7s z`C#|wKVXz5*1q`*JI`bMKs3>P^5=we^8g>^V(aG}>y!z9vOln(ezN`E>ia8Ko(XJw zhwiJ}i>puk*(+|VbLAy~C$-~uG)Z;qOY8@u>GsK=XYP1=cdpD6&g7u(mO=c{2#;FW zA)>!91ji&?$3b`#1sAvaS6=y7Pkp2Oj|V+70VhuydJl?CasIj|@aor~T&H&g#yPl) z3C#gTm1PI6a&curOE*MuR}vd{jvdRm!%t7V;`i^dub;SLPO4h!ax?;XxmSMNDf%H|zc?4i1n~~J;6F&k73B%e?g&Z{x{e3&HUut?bwNBQfjA3KF3Wzb zc{b}|izh$+8>qrVYD~5Fm+EK2uliR+r6h8P=}Q}Mv_}Td;P>8R>)`p5ZKz>w&Nc$-_S5eu`wf!YES+?E{x;IWhM|N_L`^u>wSylv8D|^hWbP|Z0(aO*w z6XUWl#x0})r|u2yxg;Js8sL_VCLat&HXB~|M(uomLis(#J&E{$=M>o)Jr8p@?qG~0 zobj%js1g&GF@OmZ6NkA12NkT!X|eZ?%=o?IYFqI@QnbgmYn#)qiax)>q`Mr%wcEia z`mc;Ch}^Gdaz)@yh)Bz89q++9wxFxr9WNMjykM zTO;t#y3}A{A$WTlGaR?Ud=8|Q$5c7Km8E{q3VjW7+(vz@Jgk^HZVplQzV)`BCbix+ z*Gqcjj5-~mA#puXVj?4PRij|SimoVh9_sz=J(j1nhC;si`6xqW?!4V@k zpjLE39zHkt0r}i0`}v1gdriFh^CCW$n>bjVM|;o`Mbp;R^kaY2goGeGleGEr!1J~) zuUA^|a}(G9Z{{ZmhtHd32NxtZsM;X~@;vv8Y3BRb`}5;s7XRvJqv)_R5#Z%9u+^(C zK^32`>V3|HbGG&g?fyA~;Q5*3TYWQ5lFFT--E*hP=lNwr?y)zBZiR}(we|mRKio3k zbj;QBbrV2u zSOd2A41=~~pV$s7vyC@2Gj+Sa;lppdR*g(?#g^hs<2aqE$bj=yO0#b>ieaz!#~2;# zoXKT>xG23Il9%bfduaM%n&FBoVfR!6GymS@PVE%oYOlr|^0Kf5 zcv$d6Tkq43G!A} z)MtN(a*QdD?MY_WWTg8Dt*}>PEAYJ_0y0}reJZF9k6s;c57K@)p<=HrG}}`BG*YEi zfBKlAi=6_Tyciht2Pdp6Ew7>Hx8vo$%bnZarU@LLUUkGs@g^{ZkP?*B06TrcAa_82 zJmBP;joEBJ+i{8ix2rYkmt^!zBjly_R487vO-;;!i-`jFx6pLYJ~ZD?h;#Yv`t7qn zEK)*Tgz-;@qR64~6fiCA!Vv3$Z$MjwrIv3m5)x9vLhM4QB;?QZQMKP5fQ$V~m8-WP zB+Qu+SSW~^EOV4?&2Vr&lQ*gnCq#tw=Aq0;N_<~HyPtq)=pUV%i*I!It6$-X zzb)ZA2`*Y~{--gs|5ZBww&%+KnZv)oD}K>NzstYv|95Bnzcn*|g#yNh%LVJH;rc9# zbjV;$u{O`x@i&yDW23>}y!0C!8v{Ah|98!Z#Oi5h&Dh!4!afNqGrwdYi)?S~94U%X z`tQOp7H4cy;coBjyo`lL^jY*;R9^)JdCS0R_;5D^v0nYpUTjcT9B_d2s=w^u%JRwb zitPBOi}pa$|0M!hRMHb>KT*j4+23vddH}tr{@cXrZFN8W_j~-aLbT5Kw=uur_}_%< zuQmR+y-fOF&;I}JL`s_fZ_kW6u?_n>u9pn>dJiCu{|bm32?=gC>w>UzQHLFY+COb5 zUb+9Seax5f$w^B}v|p~Tr|WE*B>V7xO8EKB09wLVLtxc_MX!mbKNv{l+Tx+0yq4c_ z{ZdO+)*HGXAT5S(Ae0y4*xAe!NP<&E$<~>}1pu!Pb!CdBja%XjdLeij{vC>&8#{$K z+9k{C+F{cRyz*{3Lbf{`rg#0G7deZrH0^DJV(lo8S<{BJ6gI0c<0BOHTgp4i+;^t5 z;4xc`E-!Xyrgi$)jH}->D@yLagKZa-Mu!+m`C&RIhB)vDYUc6v3R1S$xJ zK>G3}Xf;Fbr$n+Xn#Deix?$M?%F3ezg(ffzQ3KA%+n1<^J@qyO#Y%0*zy+t%Ci!`S zlE!{#$bYPFn@VnqhA$=s{$a&iyjDkSQVS<*@5SjXmkO-YS~8xlQf8|FikueC8VcrxO##q~;= zc}@-}`&11NuTHpre2R{lN`}@nHtQP~jPcm{M{9jF7>u6l%uh!b#8I-VS0oSKpFa^G zN69j%s6KuA^u44!GiI%`V~+6eyvK*=@Oew4X#`WEsr__3D=Pb|5! zo50}U;B~?hvlw1f$nn26?gYJy463Wx&91pogPyrfV2x;x#N;e+IS?oN05zAh!(0!hhb{erUmyT_labpN*P>*AxCqq#{MZOM{rL{W>V4_G;9e~yV^ z6&GIaBfgRu<~?4OGj2n5&?^Xc1w(8%JVI;@92Rx^asrV^42=#&f@Eru^^<+f)Wn~# zh!GoE`-Fcztbsu*6r!>7AtczV{zQi1vK4LWq1*dMXS{6SAx`i-athQlM)JY$tP!}iSWOZ#sZz`KIc!NI2^2r9@Z@*Y1```0QVSw#JglE|Gj4Q;bCbmCDT&A$CZnrXzFt}D_k$i<)cJ8pwjY|90UW6!ucg1 zJX+18A1~^=L9_3-4^UR2lLf4oTwt~K7>S_TaKKsp>vS3k0GSPx_M`il<=FYI+WglR zopjepVAFb7V=enGVQGRR60h@mvNqr(SQ)+bI^3OrGxesM-sE;41pW>^9-WAaVU4D2JL3Hx7sCrpmg)3F-2SAB(jzb!clA=R0fJ0i~iNEcy zgZz#@4N)@Isb%?({#cIL#vY#8QRQJ3npr-AZv|=kdKFfNGt)tRcmcO<47f@Fmu#=A z(tUnrda28Q{UtHiDa*Z0#)jAlZ%Ym^PSJTQj7T92mUP8s&~f)T>STBktKRtcVBIe<;E^`Kt&dKS-K?7n!c_?v>;A} zK6#7K-J?OYtL?3yjyo5j;pX0JRn&9Q9m(rQ>NGs$VDqe`kz?0&SBkl+Dzy3Rb=_AB z^7vY|;+uVosvU5(dK|c_@E`R#pr9%RmX2ZcpATAf_B|c^=(~~q+U^NaOP~&=_>s0W zXmNWuSFW^;|L9Ze+RY1_xD;bWC8ft=D=X|*NpO<~q46owF8T-}k=627)Zpa^ zNT#ZvAHJCW{MMEae9jTK>BS~(-AQF(Lv^SwDBNYLr>=_+R}~TpOMN46!eGtPY}M?) zi~H;T{KL}a-uY6s*skC)ztzhlr|%_sG2kL6@uymh@D*?J&*pPgtpPrh0Hv`}R$ht7 z_l-8=I8_L-mdpZ})aUqv{IE)8A_)C>*}vdHj8PX!B0bG{gt|MlAb=fbS= zloO#62TSSqBmidNS`|SVhF-VsmrKL>_r%{zIKi-OX2*f|-_8N-h)Z5PJ*juC`}1|? zGUeGp7EZkIVe|J%TiWrvf4GSq^4c4|4efii^k{APR7DwKM)Ty=G1Ho^wj_sHg<>~O zP5xezclRfk3g#msR>k4N?OHK(wx!%6Axp?SW}24zO-%gi_mZM)Ak&DWu}9cuZ`sO^ zYP5~=g%@qd8*0A`QlHQoef)`tKyY$OQ&!eegcixSJ{Rl=>)N?UNXi&rY;dblQgO=y zpD&=*KANLswr5%5tv4To71XEWjZBpAN)zNR`(idc)1k()k@aXauk{EOx#e=zARFj?(6S znVsvO=v*2!?LZQ&xTQ@MWUKXsJ(CsE0LAX2;O;>Uuah6}q~%9NbE!dBC_Tv`m|08s zOxfBx`WbZ5$lYvtY=lRgbP(MS=}Y?{(atoEn+Lkfrov_w@hLP|KIP#`#AokV z!KV(8oic+Cs&Al!>Kz(@=^90$r)lL9`tbXwZKo5NZ4G%T83^{gpAjQYOvh&ly29~9 zlRR5PupNG&sp* zA}Y5v&YnCMroZVv7#=ncPqlB_t=_%SS=FpbFP7s&s~3>QCO;R5JK5fJXXInfdUB!v zoWZiXJs)`F5f^bLB$}-~YGa%@op0BHjHfAXu#e7d>4CnHzO->R$2t^yrqbr%Y}WVB zpop)f23~7M>vP0~Tx&}3$T5RhYt|WjGCg2MEDMC7jN(>eg$aE|ke_@(0B;@$PByMU z#`0maZw~eh(>&2P)tuolDIr8}Ph2(O53rv>8-@WlIqhvt)ggz6T&zpTS0Y?Fqn;sm zshAeY9!`E=cmt1fxr{rhK|x~3Kd5eR=l0L2wA5H@Ia|ZKXU$C0sI)cR)22AhDNT3SEHIG~l{P|)ZLbwa>3Y9w zC8kxcy&cWvk4X;!R4v}R{RpYz!m(f&PQfcPIeX39dCICX>tN}v5?;|noB7yv$pA%iErIK1ww<9Dt?ne{znn9>j9}XR$^VTTrw94_->!*)!hG=0r7r1KStx{82fE ztZ0?RR{!-2(I$n2J3bDXaWCbzEeT@V?yn)kn)qc!uOi%~qWtDL#`e5V>CE`lOy5cl zyT`Gab7FS?CpvU(Nw}-HWs3Dg1A<7{X{A4-LD1+28K1QS20#U$>_fms*|9;(Ytx-j z&u9k%@^}OOOwLjE0>Sgs#!0rXR3)W|AbU~sXnZjC(%FygrEar5*#gyBmb zVV)TwNC8K!b@p@={CZ`97`Ru!{G`>p2H8A)*r@N}D;@BmrsAklvv~(^<)_#06>Fwi zAJI)-gJG*(dGXYxKJTCaUJc&mo{E`ANS7=h_DA|?JW3$LE}llAX#MIBDohGB!3w{q zR@5%^{w6Qg=RO+GVa^zW1PhCc-(Nqxd7JmD+a*m?^quIG+D9GIJK@TWqf`^1XZ9Q5 ztxNwlNN7&4I_d3qvwHepyc*O9w_ zi!bi~T}!ij_SOzu%AJm$-e1X=UaFA?Bb1s*BVmS*h(qxjN3fu)b>D+2*fo& zm#9xYzL!BBRHXXY%-g)a5*eRbk@MDw`{&i%Z2$eU){IZ36;{g!_H)xVOu|;rloshM zQ8CK8g16yvOG!Dxr3sY3o50H&DDcSxA_xh;wl7;~3d{DVwskrwV*5SIBTMvl9eX`4 z7txaK;d#R!Hd@RzD~tAIp@+~<#r}8aOINk@uF&?ev)`o$dabn?ftTfVjaO?|SkJ*< zzP>eoiynMc?(vM>uf}roZI#MuJB_&8t@tP&k2P_lX+3l74`&E_dtdu)CqD-=sd-`! zI_Zdm;_Wpz+%mX<@!j6inWRRgqq#yked?kbGW{dK$?Ey00D5N*ll4R!`rg8)4=WrQ z_LNxkB*V^a@T_&siA#g79Fe|Pkb8Gup${Wc&n$`ZwaCX^q=|Rbta=KxYY2pOSFkM) zeG~jacfg>BJAYrd8-taVny_}y5y3|9NmA^W*kEi7*{@9IjCnjQ3;T2Re$+><aHgJH|fwD+a%huuoSdg#w>{;OK*tjJp*1{RdkdUDK{n? zjiS*`OjK0yvXEE;j!*V#VaeJ& zSD!aye==_1RS^)pd*YvlMeiTq`1-20SnMrd%G>?#=D2iX39h8Fl)?_8+kKQ)t*xIH z+oNeE?C0iR_8RXLST52Ttj-SwWy)PWN+~^AssAdK`SHx_M{Lgb*d7MiG>>&b?Z%FQ z7OdkPZ;c9|cWv$~VO&_xH~Fe<-8ixJw>%W_@=E)`mo(tt(!%X{zKwMir3{ zI{MV#0V%3>fB3+7^xaMC5DEbwcF0g9$H=Am*NnYS6+envzz_Zi&BYnn;y+!9I&bA` zGPfXJT<1Eq_d*TM^P@370_y{>z_FA)SvUdjJK^lH4@V5gL2fJ`&Wy zkNmczvoC^67lg-7KkAz!iUK9(R+rS=r$Tre!=p%e|Ip9KZi&|J<#|rxaDg_YkNvMH zP4ru19o4j2mc^3rsUhL(KPdPMvhrlX{6E>l+TK%F=;7eqZFoS>(!4fthb9vg|Dd`8 zExP6RXCz^qu}cu9v5p~kd;7WB-){y^6S5?-7?|Yw+@@T>$pkh&`uOusGRO>Whsvc9 zq%QemZ5QFr9%PZY@QiPG*m9Dp=w%24>{+QTEp@8o&{G1*{->1{mhRSoqc^;RA+ISG zW_hp(=!UA^$R+I*ZqzI6{i-aIuz-jh$+O~~X$K9+6!M2pYu?PLw4JW~ki5NiZ>tUx zSwvj@raRliGaQBCrZdP{QamI2lj)42wwYc~;()bC; zSX47)LX{F>1IL-ZCsNt1wsRLR#w3vxS5jv$D$AFKN5Q)L59)`1SdwBWpab8`qiBS` zo}CFO*BJY38edG!)>al__#bP6vq5B}(&)t0ylzt0C`;Qz&x7RY-lXL1j#tA+&q?dT zcySMAQYLwU3JZ#muq2&~O_sjX$rU{n%S)$U_EbOZBF%kj6=4JU?2zv6Tetzm@0N60 z&YZRGJGzaI)Y&R0dnWE>9!VBic6$CVa8k6iTCvMwkph0c$|m`U3vw5tHGY#&w9>W5 zkRGLYfExH~a$kU@5x5Fe^Te!ihratL_8hc|k=6jfIWo^ifXQ@VO7M_v4LrVGF&C%6O;5IB3r{Qx^*U$$1h08Ye~Q(@<=2Y8+OA+HZoeGdEVBtvhK)H~wz&w18s zRK_+B0YE18|){*O{bg8q^`^;0iqzC|(sl8Wpt~vojR}?jTOv`ocXT-<8dmKSG zej;FqnzGyDbUDj3k3kExpZr;6EMsuW7AW9ufw2j;N=&h0dWmaRK*qmyXrCs)9x)Mu z*srQ$jPAXX9Ha-`iy%G5ULY29_!1J7)CXC(-I;&va1T-rVgha#aye(- z^3kwHkyp+}%y&f2WS8oX=?R>DOVisE_gQRYBwQH$B_dK|=P1Nb9Bp>|PFX>9YoX*R z0l2Kdx?>#NvS!^pXN})--S8;d;8RW)1ic_;MIb0@WKT}{In}TBL8tc*oxAp^xt`+~ z(Ul950FDx$n%C^Pf~n`^rv3TNwW7{59!09@T|ZwE*nSPK13!oAaAbI3CXQv#FCy`M z=2IxymZ}rgKo8<5p9`VmMLTHr7L!=v*QJ{0sx8+RWFr!>Drh;JvelvZnDMrONma#= zaB{uh_G^_;g{5bEDyHG{UDZ+6L7M|_9xg6`{A>aWN=&dareU(r{7GC%ZIDIE``IQ> zTbeKV=&*9(3&X1gp`-{o(S(o#@S0X3p;>Y-{p1kprx$-u-Kc|&k zSj!i)gSZdSaNwsz$Q;r1=X&zXAg?Hs{Qdk5X@^Thpqg2TW#_7mRMcRu$s%lG-^qwh zVl*Omt?YaU9wElEMP`){zt1va`xFAE#36iMH@+~{TOmTf5v_2t(_Mt?cQ1Tq`M70- zkhViC2{b?>TR#?%yNkL31R>O43zgqQQHX_eI&;}f6Z6(OS*F#UcMfm(Zl;Sd7B-SA z!wlcoi|GS-Hg?=`D0McDali(1t%n?RcQ?+yF2+xjxoJfMO|*eQ!5F*QBh!;O1H&VX zOkxp99h3ohoXT8>#qISGG2r?`J@`w|HN^0}a3e8H5gO0`4a4-B^`kN9@ou4eTm(q% zd;GS{4J*Ow19q%O(Kzwok5^-IH)~EaWy*~1a}G||R|lfCT&WXLf&$vcqK`94ndtC- zkI%e==8(SU@&BAFqt}PXzz--X=}kamlX+K$cRSpKnT}lE7NPcjGB+L5b(~E?eRhtP z-vy?>0~^8e<#x|Pal&+x65l8%;G~*A!L;FeCpssm;f-~~ZAN8FqRPVr+uSl^${99} zJx2h%Ps?R0tQB;~gS;QZ59gOWHlOfhNPJ1SFsczqIQ65M{Fj`gUlJbK3T?pyIi8$3V)w#`7>H`@ipD;1PH!%aG+16)*XenUdGA0eWo#D zsr`n`cTlN7)FJJ&xqP_76OSv8$(059Ik&|3g26qiZ0YEViQ=D@E(Btc*C-#K4?@%# z;?-i^-u=bt{>T8QEd94mL92JO)3aDp-^}HodejL`j#otl9++%tbei_x{$kBf=ZF)C zs&#IOwwLi1BDZ?$hEnn!cKQTLk-6V<3Igp;bclhStY=sn&$h_rH{9tDmyvI5M%;eo zUXz(N19^EbM)(Ec2(0EHxL^hMi78e1$#?rn;w3~gsK&~e6q#?4zq>lr7hE2-e9tIT zN;~u&XUDzIx;8y4_gxM5+p3P>Bi{G%e)G+^xgw$P6ZIGLPQih(XLqYIL&h4Fmfj-I z7B{zIat7-Roi{gJeWTbF(PZmvl2kNaM-`yXHKn8h#5%gXbnySbXK*vpPFg6vE==90oj%K=VdUlFsbu&FNNt%ghpfv&B)NE!ZCD;9pa= zwKpo6+|;P&*FUeb;YR&gfgHKWlUx^bF_O-cyAYn?-n^8A7 z&9@CI>Dmc$=HF7^16ObOex!}Rl9EDmKkJF3D4HVs6LXJPu1+^)(8*rrH`(DDFgJld z16z}aXRwb>HgY;ydV-2vw%C-AQ5$%Ebri2k-vd8|yw?S87QTx_&vnSMhnM%n^rYup zIh)1HKBbeXw&M64T0uwse%Z3s#h#+q-$CkEJnrU|v zLwfI9^ZdJ&ouk5&(~WQPeim>f*0RQly^E;9C&x1Dks55@%`xk7w#v;joMQLUxo%wU zQASRo(5Wesgh>HK7*Bqg1>@$J>9Aa6<<42jO%%5WAwPHiT}qAbrh!RG6k*f_<|%!> z9#Be+desc*PJU@l&jZ9~+RYjhBhP^jSIbf)sLx!+2%TnP3dd zU4yeo%+F5*Cx7@1oMIX7+;gjFt~+^G*O$9%HjV zz%`sE=YX=CDc9?sRN4PT(041dUDCbN6&~t(XgJ!B=dV8@m0np zOqUMCcjq-2u$~<@x06FYKF8_9ZunS@tY-5G{39$pCGVORCw0ul%F~i#PiES+;oc9h zwW73cMl*_wzoE9$M&wvU~;P4r8PO9{RFl=>2AWR_ll0T&foxd5gK8~I%?G0_c0s##a%wp-h1-^ zeS(`d$T9k$;OKwRMfF*aO?MJg_sGs7(3m6@B7ZiJ9+lG?9tn)K-v9pgTg}93>rP<- zFd;?foz{(FziLkR6kyECs3dhM1T*Hpu6jS$upXA9|H?-6Dw4ok>K0Q&SN0h}i7b!V z#Fw#CdX(4W-PqA%&-cyo-kfQMCtiZM3FVon(LXt%j({4uC~UzeF{gt{15k)%@ImP=BtqVg&|l7#aa; zzu1a>o)^ig19i*p<9*1A!543}^Kq9W-uVq{J_VqxEbnQxZI1a5;OlC z(kh$Fee${CpW*h6E+4geQWP>(11M?Q_YbgzKeI`tX+q^~#l5c%*8us_;p`-#=~_(* z#_!7)dxwfwig+lFj4N*A{0;_d-u9N09CFm^-QQ@qOma_B!p%Q?;_U^j>jWUqo*M~p z;6b9+)hmEwNU63nKjLNNHPO`eIL?)aRxOD9oxcl}$st10W|X35?;8{K#&vLmGe%WD zr|%s)oNV;6M~Nm3$Xv)o?zZ@mOu+r;Rmj~%szlfzxe=_flD2z~kwCt!QTm=Gs@O7vcuhp~u-9__4>tz>%w<) zPNcDlZ!5oYAnFXuTrNcPh4O%201gnJzPAdAf#2;irzCUrR$eV*G|h!3*8E3lMSPKG z=BF2-cjTx~daQ7tGH3n6S1Hq9WvrMcDieE%&aViK5ib_)5H;OSOk@O!QvUc%ye7kS zR*MX5zmCUJM4~n#oz0z`7&i0G%Q`wJizQ*t0O~Y2uqtx{GHzprFHoaub2H9^*{NDL z-)FFz@IF>;10S;74m(0?-v=m&HpATg?ag*nCSQn(iflpiVH$U9#IaBG2R!t3&Lej> z@2b#H)kk7FYydczIz8*i@*j8h+44NcO>L8l%=clCIe;?X=nYxwSo(0jMGOZ=b-q@< zb7$A+kf%eH#GeRefY*d5nXyQy5zKcwhROoLXECDY1Gh+`k?zQN+308=rX z4cCnnxvUiiB=@p;i){Q=@=J6MUd%wjMTDgouQs`;k$$5PO26AZD_ql@7fI$XmQH%! zd=G{--QT;a@|@+amm>VM6@B*e5s>@ACLjStcA!gNXW>a&jJ44h^)TZlvxKJa} zQhR2L;@2ExpUe~W*x0NrMqGuL{P}5c9&|iWn0iL&vXxRwVBG9;hUYoFTD3rwPm)oXZB&kB>vZQC7!EPufh#zfwhb zI|9#Jc17F#}Ks_LwkU8Yn%0|Pw3saMpLC78PGpWA(@ zg&eb|`UyT=sT1g`hZq4}?(+06E1R_0q=G#Sv{I z2@*kl4`bz9g~Ig0A79MNj#)2ZG@f3LshZoQvr;Ltr_g^foL`(9sjy*7v9_R=1V@=$ zQf`BT5#&$0s$L1@)=lAp4atr>oU3BLQbw;Us}=bmNM4-Py5ev7-)XYJjTh&LFKfrl zAyxB=Xnfn@css-+W^biQ!^@VRROhveXf}nsNW724hr72P9(2dTuqRv=)ebcik!eB< zp;N(Lp}`$0PIQ0%q~mgnxHdcfo(!JWmUR@Bzgy3_6?HA#)fBL!(zh+~$tH}I(?bn! zw%s05mzA9qaS$?+M9{BSF-B_s%ydll^r-XLsA02Q=;x~ujE&ePl*uQYjv(cHW zr9;o|-HJ%BVt6FlR)TDVT7S*A_!8iA#>mmlr*9hxa)xCMGug&sS-y;Itg!@2s9gpv z;bSoqFwL(smbXSCDtKBjn}q5wep}XtFCJ3JV?%xxmB2It>#p2zK67IHup+UnnWRkL z>Jm_+$;`35v)6DR`zGHYatjY*e7CU^U(fv)nj?)4H|jFhb(~}KqV7AdAw%*r4bNP~ z60pfvy9z~CyS$~PV%|7qs{7?fJTqTaM<6_^VSH7em%95%@VP1HT%rh0*F0Jj#fNYd zm%b%#)$rHxg$b@ca{m40jrjil^Dszn6(4R0Toy)QlCjXa)m2!SkdhfYX#)kn;UX(8 zEDzP(bJ6-}8p}}osIoY&=E*6YD@&7-l6qkkVV3&SGA~tkdTws57J{oOsLHL`640bC zpMhgo^_YP$di_O(C1eu?A#iGrSOf)-gZ$m;@7_LmOJ+IsF=%WJDRHPJTIT!2U!GqKm5QX1=zUskB2?#u$nkKcNL?d(Cbx;Y(VJVBXwM!*ukE( z(w5eRCzYw5t!B&YRc-tum<`4MqdolN3#eFUCR(l&;(d>$HAx0x!d~ zg1OQ*uUYTw`|Z^Kc!W3GPZujtJ)qN&R3-;o6~;mH$Nadk`;Y9;{-N@*;dk!Vv>Ahs z(?(f{Lj;}uQ-DDpPjit&1n7Io+J+N;J%Wn6X#EW$P4a^z6Lh{#D{t2L`@+}5ny>2B z94bz}kMMsTCW>sjP2p^^TM=d5r5}tVznKe{%PS8(Rt#6q47R3hN(^LXxx;h`na+JY z$A?QTTq^RN@_NM9*_nD$wf|_SpsY)7=us=Demq}r44K6^Hbpz+dIFthg45k_`o!{L ze6$h%BTQ|A%Q!BIho^RNLBm7zClD`XyW8nJZtKY%(en1yOfH%~y;)sFiAko#_2nJo zj=umK2}bhl;5HNP?#7QIqqq4k!QyO;6JU;Y5& z^e%(Ye6VL7>E`9e@G#5r`YX!tOxEaVZVyj7FPiN^^9%|vEE`v%!F0+3{JjvBlNY{6 z#ta4qj_rwCcLJ_OJ8?%FuQ>NZ*70{+5Y=U4tA{M zH6Gk=52BKQyz6=MCGXXE+PuVbq<`^ju=&_`55#ts_qMeWKq~f$MpVqgYlM^D=r57n z-B9Ss`mqcu#T41u>2meM`EtRQW>XX&}ro&2i!Y{SO%=aUp$vkV1a zotuh|%!y!`q1J9qadic<^mMYE$E?XkluUGgx&<~V4sp)Vw#yzu$s}q@0Cw#xNvHnft&4BM~eGra}-vZ9jCfvD{eTmBXr$q z41FXdTk+Y)%qE=~^%|&n8PtVgz=1ysqH*FArI6xa16$E;35l1zPz<*)Aj;S4B=rkXq*ku2lZT`&Rg%- zK&T@)mveW>7OFJn-d4=$w2oB|VW>3F9uay_BgIIqNyyWyIMF1Xy% z`t(61Cm;nwm|E9{HOGqn_;LL=(KRT2%mitTxjrU%F=oUOYEBnRAT7)ViTT4<Oz ztjZKUb!agZhG#iPalJ^Ij0AQY6-^<;>h&PR%S!= zH^G{^n*Y3U!qnF#EPZ`6sfx2p}sx_Liy(s(;(2O4#F)fahEUqi7#Jd zC#)QKPNG(2R85WP;6Y(#7on0^OP_KEPLEC1Ha{I~>RHDDg7_mluSAgdizlZ)GRMr^ zGUx#`-#M2gww<-hQf?cMH<0w8cxq|*(;&p0-|N^-j_d0`QYFVd98t};SoilarsoGI zCa`*9Z5L>d2jXjlfth#zUPVwy^(r5em%DG!S#Yp@O0sul+a6#LxgoK)K@ z5#h%gtHs_>A1-C~Pi7fpWOTSpl+FowyAdsn)ED@+=LLycM%}*wyk_0ojc%}1RCeVm z5~IWCAz&w#ml<+cRO?0H&pBIbJHjdHeiE{DJ}aWPIs?L}UrCvk(DE>)o%dTD9M5K? z7N(|f7uwHoc=Y~GmRJ)d@78l9E)#+%KN$Qb7!a-bEOI$rK? zP%G8D?OpZV?`ZWkvnRMC5XgWDjI4+Y`yI@JT^&GeKZvlsCap8BxvLf(3W|%@E`vO9 zh}-w_-J|qWGMx`gwJ=!fZaV9 zTt-cwF%;^I2SOuvYF)-@Sd)SAyL&QUYbxo`5Yj)PBzT#l!#AmJe?}VN$(5jF=KwgS zlOA7i#5`F=g~9&S(RVi(Rf>#1{n*k2@Yry+r@~U7Rs(G-rDtOP)D~uOk-=T4XSs;h%Q+Lib8Cl>t`2uOb18P#P|vkG_ct zw*wZSYyaGO5;>`M+&atbV((w_Ot2|3gsrizM#ODR0v2N~>e7sRh*%j6idOupC#ine zF6!`|i!1*f6It~q=Qtk^^No_On9@%#63IZpyr{dY8@RXgCQO9eVtguIm7Vu{!K>_H zE`sjzv6|Zs)3e25@2Uo2<@?X#09AZB= zGWDEWKcCGy@Oo*sV+;gBl?GhSs5EU2^Q%&7ztew%soy;Kt~90UPjD_gj7_-4L1T-3 zCH8MD1shG{Nnxxj5B!pfJn+8{(&u)r?^SYZ4o_YlkP}(yiR}=K&y&7;-fS*2y+ukA z_aRPWNif23MO|ZBTKtY&oMFSmyRq6_EebIsIw*~Gqi@@kJ&MGT@pBR(A&F1kVyZmJ zr7|?IwR8IO(FKip{Nm~1RaZ017oPP!GCV%%Z4jJ~hK`=KU8GdP{$OsF;?UG%t)*qp z$&S&kKf-*&ZGmHum(lYdVuP#E65<%QuiZbMPyL=)db>>)qWHTil`zCFyA#YB<$7_Br#*pN zriqO>`cYmQxx>^!BovIfj|j7^kRd8RcvrnXKm`fMk%sPq&R=!jtyZ2w`Y^0o4)tP~ z^OqUCR&JhUiX8fwW>e-c3q*p~5rssJeujo0WQqOv?-fuh=XZac9A42x5-!#0+l z2xt?y1@7a7uGsIUV5IoI8>5YUx98-{Ooo^H-E83bR$OxYJ13{rXFJh%*we>08c_t< zr+1J-i$+pygEeM}$*ynx)kl>@<@M?9F5$jS(gyMie3-eOoVnNY%lG@UtLwY7DcTu_ zubi5y^T}FIf?`HsGdimxPZR$hgo9(YYO}LmF4+2<^O&Fu795*2Bp%^8dWb43(Rn?e z1#M_obeQ2<0+yMx!^c*NDIk2OG3boj8j%$SlMnT()nfm)WE+pm1|;u6<);z+<=9c< z+-kg918**zNk>Ud`Z-(wI=IcE##s8yqJ1?7>NWMd-BJc!@6!r5$$Fm8yP~+nMnp(h zLTj^k*wp%3+f^60cPCIto+0w&r65YQNj1eq=^pn_);RG-$KuZmB;Y0EEY?A zPEeEklmua))2c!vh<@?vv&>TGu({qpV{`@v`})LnMBz|=H-YL8xP^0@J5zg`K*-M7 zhT0fYr_U1rnMs%2Y&K-B&Pt#hyIPU_^1w^8vorbB|1lV=m@_v2Wn$9%s*Rwi0Y7wj zX`cG~>ck>_5wZWQyzulzRr_?z$l4qjH%Hll$EX8W;Ds`UML%bpz%&9=3R_&cUI zJ)Wg=rl!yizV=e>$6#pWkNhuRwme?^Xy=@-uPiO_8o{T+-3(vpJlS`$vw-d^_jJEX zNe*6qUC=hApzhWAtzo_^_3ba1npc}MX?qHfMSXc4#mpmFtw=f$d21skwg$6t-M7cZ|?C&G0B?cn&AO6w@)j8QVa@1oY zSJo|$BvRo!QlRyCU<%wIGwdg4WDnXx1awCS9N!KOQy!`f58UC)NBg(X> zPL?^}ur72Q=p&{wdt6vi9h!phT9Elw{v&!OMV<-`1U~Ky^L^+`)nBom$PoyhsChn2 zlk;w2u<2YxGR{E#O-8JR^U07^%)atMqw{)sjOg;yj)(}$(8Usyj+pFZ|37t$;nYyk z*};FBb*h!2e5?|o>n4#+wl9~j@u#V0s)w9kO6F7366Z%B{#qLRCRG29iRcu+KT#;B z+FH1Vz&{4Jt84w|k8)3P&Q-g@63Z@9Y@6`42E4b$1VO7_$&4$>`OBbtKF zephd+kk4Jl;xQ4aW0tDlVh__oDY1ZoQh62lZ%;0nt2f<%{GSwG*it&Fa)ys+4IXDt zJ1o&L2RzgGgqWJBUdNl6ZK&nklMDnW(AxIhf)k`{`Ol%O{9H`xOHmsxGZRF6`N+>^ z*u~_^{hINqgL*WpX%THB+w|a#3Km5kO>r6Z(}_t?T&_VDME`ls`Uf0Eu?B^suEp^UZgKKkzG&2qwZQp7p2 z2-|NGoA7Ngk}FVaOE`yG^9i%bFGX#7OidCWe;*~NX0ikP$8M+(NM+Ly5Qz&*!#HJb z9)pZ@p@xoYp5o!DMo^5|rC|c6%q~9D{9N^?6p(P&+M6X$j4jfF$qs*^R{9&v)$66B zfSpWK-`Mdzpc4hly*{S}ZD>G!?_jGzeU(8-?hfBgyd?z6WG%N;+_m@Z)~MmYPaIw= z_16UmnJwZTyeJzNxfc+5`R+%>fkOTVsi#*!!=DFxrbx|&Jj>?8sfgtb+hscsIe#u)k)eF5BS)G3JSHUQ&cV{Jx24rM_9p)Jj{kN4My7Bo z0Fb?P!3!e92fn2B94YfV6Z=Iafw^Rfmax+qrpg+-KdcV;67ZkdXZde!)M&p#-7R)< z{Z^~KKXxVZE=sVPb8%h6JGs;7kQjsv@FxxoaJ;g}m^-XmuW$5yfF}!fRBq*6NV@^2 z@l)>4T@k{9#1~Ds&`WPwcWn4=!?84hb+R~T<}olmcWOsdqZH_~WEDV!~+F!ArG z5F(cypaZOXl4 zo&9WKrp7BbdnY(5{g-JEazw?aX!<+94Rji{9r#tdU)^G_u697c(Z@mk3!d8hWq$&D z@1x=h&^nWqyXXL|H>(&6X__l>D@68a=(5dy*7ER$T?%UWajz6Dv z-V&oDZ|^Ms%T$cH0Xf&Fd$avGT-<<*@}x#?Z3Pj39CDpdf@EHu4>1wApKwyoPc5m1 z1rpjq$XxokKe}JGRcC3XeRco53%7*vJ;JIeAQ3pPIn7vpmti+oA4pJtsV3MU`1`$1 z-?8K3&!vWJ$Hj))2Op=!#%j-V-3F)m?wA%YzqUe>98b*(Nlcxb+it@SV#uQ` zpP!6#^IZoAEfKv>dxx>JqqCc8Bb9gu7*Q^fuH$mhhL1k*(blGuY30`vmJ~|(4!seg zFhyP}m>kY)c4aR#%HVhfj@c^A1@tY?te_(oW09rkbfIIa$+n_#2qUNpUhm$u|6W?3 zUC&7szR>dwjVH=9jPQSfCMFlH$@0FTeTQi4Cia%@+C-nfQ~>XuQbskI@spdl5-_gkJ1?vooR<1$sxq6*&E>y5Tgcd}F0|BuS%1n)g~&-%_X|1r-BvC=^e(m; zPlH$23pw3|-FJ95W_+30l>}UHfAYjcgHU9MwjdeK4DCI)UF{2VPa7e^Wi)U|x;lnC zy8Np)02?m{+FT120QykHc;9KwcjZ|8u$gqS6FDk?rGM>g4f!Oh2-JQaiXpb;cQ<0@>veBS^8^5{C;L(1v%Iqvb1mY}B5hDDCg-yxOLX{aV=6-5Jrg@e}{QjnTKLvij zdV)?g3fEK&GS6lAtfk|mc~3Hu;i@rgKZR{je#WuWoz!hlDICFnlK03fHCxw^a@)H3 zy;Tdot8t|4Cn=TV=CC_@-^guGey&VBEPe+b0t}$ZU=vYa)X3idVDBxv;&`HV?FHXN=EfzgP zb=9t}y|4Yd+9~yGacmYwPshOedZL088|Zm-)%;dnuT3$K%cZ;H=&@KdrW4h>&X!Fy z(=X7${-y!}pKmXWPf!TsKz@upU4nB;Q`4h9YOIV3b{F#K)~AJQ<)wKh>+im;QFz=w z9##^RuS54DZcN&CBkF(z&Wp{QM1MIjf5ZLrQN6)Yo7A*k?ALP?thSksULIZ(I>rX9 z+q;j@VNi+J6>5s@&!Trpa zYwRcFuq)5ov$LyX%I(r3K`Fk2?&PnZbae?JMPo*P-Y>6JZ>Mk=yn!hR8%h(qkWKI{ z&VlnmPe3|4Y40acU=3+dO@JuD;Wf@ivQfK}|J1(!NMI$fIq~Y)YS-MFjoX^to z1HBLC_Vo+Hr`dQ%vrdB*;$iz4_S^#cghO-A`Ui*Q_&oeWfhkDvR-@ZmH$r(m$cWya z3-g$-5@<5vmILl6X+<;QnNqnm(|3g=XVTD`!DQrjJKeH7u8nLsSu?;R>ixJ?&CgD> zFbJ%upUDAZ*mSVQ&=F4~?h8IfR@olY9u9e28VUx2NL-8?)C)is*bpmUF4XPldKX>f z5(*`>TbE{~y+MpQTz3&`^Kf1V1$}y?v$$-${&aae8%goYLkO>^J1i()4ESb4v=q_U zk>cSJPo4>AS-|R|&dWi4PYRAE=TZ1xRm6=SWi>7BD-p_ZR&j`-X zK0dYsb$(zXy=azFv`@^kWU$m!Hzi}DpdpFIMs=B-nvglY*D3MX@pV7@@l|MwBPw&6 zh`5aXz9MU9_cgjRn6~183c$`~flDwFat>)IFmc0s!!rm>mu37Wq=M`!0%K-N5!$B$ zv7m~G==WLAg!mgyx40e>c8AGRMXQeQ-^?dxMUaw?Sn*7eaa-)$(5SY4gMo^lNfo|* zo@W{bZAP+@&=5qw#<=*aCmez&gEjuVZErT>AL8XTqAOY({nm4x%DNU})vLT$MNSLl zek3S8(fX{}<@67V$h}=05&g{j$I2?+1=}8#eV${C$8RUh!<5tn$)huhbx+^6->fgT zG?rxzC&1fP;+39-3hn|gF+{~cQU4?_3PE$n?Q{{xpe30>Dy4`+|DgVdU~)|pFd&(JAUFf6^a zWRal5fp1_`z6&j*TON06PmrOKXhJGXUy)G9dZgw(BG+JFOX&RaM+tmF`XqpHV*z}H z=|v7}=HP|*VI3^?S|!8q2nq~)f#klu9PZj7xoZ+I!ew>jh{Y>`SXg_&dqJ%omm-5P z;hbv1VZ+Y!-~!@3R#sqP9-5tv}B1mT?XV}VNzUk z<7>sX0f?(e7fqIhQ*bXw$#7>}ujRyU(6^FZvkO2&dg5BHy=Iow;_$q`6LFJ2;2ab*-^;_@z42{X7NwI#TPDbooANw!aIjJ%en)WD$O8WPLlZp}hl z3;rGJ3d!u5d#Nyd|*@`fATTznl|0<4^P`yez4RcPJgV{G#>M18E>Spn|D1a93konia__8;8PF` zdSYFJSn9ZVZ(G^uz?+;!fN?WK406TR^F51{bLHz7Iuft3rEfcN)xL~O7HTdFPPmm* z4plUn?zR9{MMM=|q;XM}Ik#WCJ-=0IsqwYd;`|Kx_tt6*NeNUsUibiEd1faDt_S2s zG|LJogGf4mJoVt>s26K|(;}X+Nsdyn1Svd=}Yr--qAdCp~LZmKfv90J2%#0nZ89i~GedsQT{6h|od9z{0O_R#+WAWpJ39Q=UJ) zym3c{M>FpaQ@c*T3`yQr*>co}l67~Lp4F&UdRnnWBSBZ>!g+A)D3GIYWVvp4ZK|BH zUBu}fL_*jQX+>$cTVA&9$<_xQj}udNc-PpoiAS&5G+5ZZ{|FL- zjfMS+yV~`V=VF#;3gK$x5kUmtiyYaS!o(xDxEh7hyRfYNK+i~ck1xpES7IcWw}MAX z)rA=<(?}IHS+8jZiIdVlrH~pPQ#AdqueLIyS=6dZa~ryBFf0tMe(Jl%fQ8tcaV|C9 zGR~>c|EU%=8(m&~zbWZ{cFf>zA<5vmu-*VhDa$l;h#VyP5+@n57_w+%@D2OZ$k#82 zrH0#WE=_CC<(f`pmo63`R@TV+z4RO-41+JK55hg&FX9>8Pe#rWm;%R=ESU#*oM@6t znklj$U2s^Zx@|r%uN4*ji0Rz}P0!X(yne#uOjTXp60CQ-%h}6^pe4V;Lz0HGJ&3Sg zul^QlZo;f*Ky_7MbQh;X9~`5DVX$WSTS%a~B6Cg0gf9@5MMDmW+8ojL+d6|7ygHC2 z?0IzI08F*NNfJKpBt(+QBL(dsdm`kQ*}kyN?18K6`lDnm2Cmi*kUo^ra4x#Y;ah4M zS(&#ijJX>$P~ay<#&-qm48g{8j6`UU`Gla=l3aXrauL`C3x^&1E^Ch6oy#&)_d1BO zrLK!XnL4!t6IH8CLH2~BX2x~Dm*r<4L4}AIP0-8w^ZL!kr?&bI($>}-P*{6~%e=0d)npJgm|TO)w%*8Ekx+?b59_-?+=nn})%2IPRP*&~Y{I((;7!ZvRQ%%o*5ItvgvDq)J zwqA6t%>U&d@*P*r67$C< z18ASQ##}#-x?VVBtH1oFT7k$ywp8V!HseEt%?0R;%P!oCihas7{m~PqdvO4j*^H5?vtAyI(fNw73VZEf5~rc8@Y()(B5O;qd#cVzG;V^F zh_;jwU#FVfZiNtv*}`%Cg?>Y#y}M2W5(Hq*RO){xuq-mot2Rbk3%XS2H$|7JbG8jr z%b;N8yY&-iI?g+5pALv;>g`aUS6Dyjux3gVCt+0{y;yUq5w~|I=Og0)&YR>nmlbAx zE_L8#>CG~ay#Q7)F@)&73#JLNvmrvr=>0Aac9Ye_Fs|+9+yg`!&CL4b)C**NHt8wlM6UNx_!lGxs z94_?6kS{{(S`!Azo97qFXm_OOs@>iOUmK-!-pGxn@uzTNH7vW@*BkM1z9mwTOM%eN z&VHQE2oYpNjb%}2s1Er9z~L&LwQNa$E_KF$Zi=m6VLSau%v9_8enN$w;Umm7n%#Mg z>mXfE48VY~ks+fbvtMp@U(WZRHNBY@S2KEl<6>hXX+pPHXItdZ$NFpU=`(O~L2{Cc zI(HK4whqKy4b~N{G9_$G!T)!^GzRud`w+I+q+ByD0oT{tIllDUI`$~|1DM)Rh0Lg5 ztoIX-u1Iq=+Vv_8wt~6^zWub?qau0%NNMXAD{rd$f2EBUT=Sj)Nz7m^U@M*%N3rpw zNoG>5I!T{F&`H!+M@@IxGwX5(tcgAo&05qgbQn>F|E!zBw~PDC9)wEBVr)G6`I+dU zgK6UX4Rq5?Uka`e;{+u7Bz!`K8;^>lJ`8A1=Xr{t$ZXSgc%f*#7XX-NB<5`H=d`2+ ztDt<|sKe4Xsl`GO_5Z;7gQk~#y^1)B#0%&O%p5(VVx$euFt@VG=hxQ@vEkZ4W8BIx%Bg&$9?*l;10@hEUeywull5SkpU>Qpk&2#_4Olk{`)(>hn}= z7&++5PLh{6{!%#H9=~t%AfVT1Na7gEuwIkLUD^;@o8eNKH!9ADC~#Z)(woDZh}{ew+e990m$5 zk5rGo7)Y|5C7k_Tp-u9qZDCkAFulAEy*cdVkI7Hix4dg?KhQ zGOJW9z##8XFxG}lTEjRw+GNdL&F-2(cx+DCn>fo+VqrJnIR~S$H)Zm6`CQmk~fiKx!mmi-b4li)e^uGAA z71d8QgP__u%%gctOT@(v=0SvzH^TzitX^-fj~V>!LQ7w?-P((!=c4cVm|$PATKK5g z7-%;-;Xmbblp}ba{dP45)1&Dw$dKMXqqQCbs>7F9B{fFbdsJM_x1V6uaEzP2fG4o))nHp$&>gtw7ZhZT8!()35ZdOtZ@ajtA&Y)-d|(~KcW z2MLPjCV@oUKZ6pIXc|TLM3R^Rnh?iu+Zsum^I=gM=2aNB?fgj0po|OBwy-i3-oyze za^W4_AOlPVIGv#SgHtym_X7rX$oxVNHo;$37KdoqQE6f3Og|@A;9GjHlwV zQ|UsPmsqC*Qe}beZXebz_{XcPJmXQtYMqOI+{j+B95dWW=43m8+9-~F27YA?q#@k0OPD&-@P|t z{wt&{<}WtO@bfFkP8R);sq5;6NrHy@Cj$${K$%Ht!4$}T$q$!hiaNEqaX2wGQ4Fs8 zq0mAle~a6cV?P_Yn1(y?=I+xyYA3gNQ%fe^N>5u*Ez9G8LX4@Z4mG~Q(MPP|bf zjjoMJ(dcSH9PHtXI|RHj)cbsd+ZhE_;>HxC%0rYrl{f3G#qdOSh**H{G!R0F1ws5O zG(8ml^=se4^01SVBYcANLLFMcpKx?FIWRm%SN=0>vP-B66ZRbIPg#pZOLR6M3q$-g zwbdd-`oFB)Y0jDHFm+rzF^$XC6Z>cZb@0qiO2s~i<#iRjB5w=P)Zpdd_E{Qqht@- z>s<>Y+pn)cxo-S2S+cf=1ognSwlk9NgsFsk2yWLM) z(f#xvxp@EnOZ+2$k;?xEmwzT~)3Pd{}-44-X5FqcE{r0kswM~Tvc3f;

}FZ#n+^;P_V*`mhIFt;8KE5ku?Hw;6O{gacE8xsfgD8hRQS|p z*RB)A>6SyIod6PQ9Zrh&xV1AH@LSvM+WeyrBER11+H_iTTBu;e``b+tKb7|1)Tw&( zii?Db1PvE0_@!i55Dmwgcl&?XSZscse}fCx@Bu3jD#VfkhDnQ?y+{Xrk=|?H|GtBm z0?!D5OLR~N5Pa5iE+m$F#gY7B63YF|%*Ope%99^{fBtWx^;^jPU!e3!CCUbI9dbEKQleOupuEZo1(S_SeSRh{zs`uY%5Vew5Y z|1)*`%fD!KKdIKxfAa~L))}NLDuDt(4{{;S&zt*eZ|IDAT%%+-m>Hj|Q|55h= zb@>1DBLFOTJv^8J5HjM3|H|daOM^hzp=MssA%w`T6nC2m1WNiLr2yzf5l?dELz* zn6o?~70eZj3O=01y)@7SG-W{ciXy6VT#K>6oHRiAG&~C%HNA2E`W+3 z8T^aF7|8LTPCa-hpOY&LM=_s7PTG;`3qGHuf>6*+PbgUFyg=qY31MmSXL>kw5eCr* z8|L&T|N9301Nr(=3PuF_pFQW~mmbDNZ=JsK*e?RqEb`u@^gt=D4hv0&*6lbL{`Xc( z!erYLQ!}ks=X9q78b>+@4+yD3tP}cfJ5DYVY64W;7}7KXC<0SkTAB}xav}E94-1G6 z-H;%f&(s{RhV0fHn(Z19K;}^NZ%oI&-g!%uFqMYNeKH%{*6)1@8WqhF$J4tP(YBwE zvmra|#3W)6#D9XGAzk9m1d_cURC{O&BD4%u=0;ZE9v7Xa@xCMdTSfD|!1snbm>3lYdcsTLzTr%vL6OJwpv~Io^LP$8Zwh_*`p<}i z5}!qlt%&2*OYE0v&u&|B<;{|?ioj0%bl1=Ewg{D!>`v-$Tc{&KF$}u+Ce@@%JVQk2~K~Dgho!{v#cBP!1e( zueg!qAi;T24-XU@H35raVIdblLOdf*+YnrzJ>OH7MSoC2>#%0^vt+5u@poVljp+mV zzepKF`dswnczmw7Yt>HQ_3UAju>;UQ39!TE1E*2NFpbmGIB-v4{FNo}ob^JIi2vfH zR}CrUub%rqt@xfdqxS>=2@Lu1!xy1hX;U#H?A)$Vj|TWakTmZ4 zsH%@V)Z|nP$b3dN%0UeyP($XP>OT@VCHZqv2HAoPeKWxk7ODV))1=)GhT@-sG{0FK zqEC|Ze~sBw0{ey5fGKzRZPx4~L8Nd4NW=`n+Pt9hczXUFug%s>eci;uY&27ktnx=5 zX`7+|s>ccT?tPr6d)4V6Tb%qCu?1dThNC(B$+ErfM|^;A=!;$k=Mg!MI63vgN(0n(yo5%t|OQ4Qi{+-#0L<;UUyN6zF$HK8ES6Z-Hza)S6!|0 zL~y5*>d66V$1LSyGohvE_R{#m5cHNn4wMdp7k3ee-2A?7r^2|D_)nSkBtk+ z#Vsw$&bA&q9e?V2JU*#88(Iih{O&hiBoH%yOY{HjdyPx~sV`(({dySC^pEcY#AM0N z5~jxM1*-;qV>CKOpvm-;rdqYhHP`JiTkP>xlfW-6(QYt-Dz_A^CHA4-y;zmIT{D;u z%tLm-%_%voKii|`*z-P>qj~(z`bO3f?*Dq!^cPEaj)%`SC8wme^&x$Ahj0(M<2M{} z`dgz;S=+zadRY*xb0$jJXF~5EtCnOa-I%64topiN#izO#P=&{dNWqiDDYxL#o zdg5X74MBg%pxl$2W#gWQ8xB|I`GEv)Sep7(HQQ#%P}!#7GGDAcql*{AfnQ7ne(dXG z*NWXyMaKu4F}pE1b;~Q?{%FD+h#5mprpe%Ni1T#&Ew0a12ahfy`@{XU-YT13+K2{C z^^w3$>ivI?M1O;@gQY-np@B5I3)@A_v7D~gD*#T!5s4rXKy+G8RY!h>mThe=q0 zryMh+BSlq8RfAYuT%5s#iCD9DoQzjDRD5ZxS89NP0g`H$``4TaxH6j%K$4~2SD)x9 zY14E&;lw{4zC(Ow=8`WV|62xlz$zXdL*h(TVpGh^QgZ7wm?yM~&;z`i6#ecEWWB?h z6xztItlE;HKjfhb^L-I3bcC)r%fC~XFz_zxQ_pB?gTbrlNPm5qg zM+N;bLAd^0$R=~S^VG~bb&>JQb;402vAV>l!0sv4isIChpvO&^Z~_)CiGuu?x>-{attNBZLu%s%_9YuK z6t5K=>mDk&sHf~Jd5n=7SC>fwc2q6Q1lUE}3B74U=s^t6f~J-Q=%_)kdG|sxER6@f z2$#j{;$gmd|El3vQ1p7cV745+jrA{!#Az6YjQ^Uh*KrR)inGCUs4Ip`>T{Qg9#|=! zIiu>{0jtFntvpHUz)tHSrSvQGuW!d)vJY>v$F@XA8!xxuyK;d1negCCv7{!`1yQ)ltEWBN#*HJ6yYT{JWBDisM)-O_kus4=4KSdw0@UpQiZO z`uBkblL_9K`3miRBDc3he~KE91;8)h?!xG46E8_ATjR|1+@eEyov3%0JR9%xRIWGs z56!}2Gz4Oiws#RZLUozEq#+5e3#CA!<+X(CP4<)N6(LoHtYJ!9&rp`y8Ez+vaMv^W z+slZ@QA~0+rBe-==Ul=o$T?w?(GX>sy{9$mJj_fHK-yB~hT=@<0<0k5j?Qp4+Cl0W z)5-@eHqM=QJ-)~&KP|evSSE_8D{;%fr)ThBc`t@{rOYdt#6Ks8+B7BhKi3q{H#nkkJTH%ol-s83UNYA zIQ8BiNsP9w3dLOdGw?KUSbeu7dAM7&YPnkYMB+{H%TuqjTV-s0RknGx;%Tm>Ws3YJ zvL3z-X|z&T!G2?|kp__8Rr+_hGe6=g7_9O4*@JJMwo?B0I2~%jkv9FVy&ZP6s|u7I zE2m|vEs=`bQ7+iZB@(~tSPUUieePES#4U{lF?Qf7TiQ=B01of9MwNYK40=^}P|$gT zJqij`VH1kAVk7GPUh*szOUu zVOgAOZ^ofj2hMFMNV5Ksi~#$`5oJ9vw`K>whE4f6wG0NnX+0n6<;?YLC?Y5%gu*si6 z%gj2{y?29eSy>EDQw-};d6i&f;h+JV6kh!(PAS#w8Jw|FueJf; zE#)RNd9i8tCxG5;bn)u)CPQ7F*z<3K1RFd21zJd(tBvwaR&Sbg>QNvr3VVSpriKLm z#3WYQ$)uIg!-ddR())?m^D2i&E9w*U--foT^iNJ(F4l|-t+7--($DLlOg&u5HX2Xz zjgqA)d;&xAv+#Q&o|zxtSlC>wFI!pkE-%T+zMUjdLR9wo5OI+~vryfL85ZittcN0=SeKwHfidr7EFz^!u)L zA&0|tq00-hE&RC&^UlX+JTE%z-0bMCj;_7jK&%NZsI+{TP9(&ooxctG(&NPP#^fEU zu=EV6QEmF}i{iyeP!a0AdsqPL`vEizP-w^Yxl}7lgv282$=yMJCW;>3pkR@7cRX(0 zO}5fc8ZqgOd{|wT)LXf}B&aT7j@p%@omubPK3$b@TAlCnVgY(^T>-c3jm}gA&3vj@ zUSF`I;#B`5bW*5x18&RJ5`5N?mSHW|e`_+G+2h7n=&qkeh}(qU>RY1pz@g&u%3?Gs;ybopKDKs_#_Pfq#Nzq{ zM6W%TV@uvw32QyR4tO~|uG|)|8A-t5I{JiQRIj10N+14&U^r_mAsQc#*Uw$d^*aW7 zGcK6^y~^PRMDn5QLEbj6K{#;~B*}H#Wz~4Um1Dop<_j6~J;C*Hny-{YyM_gqRSWgdUc$* z=HR!oJ~u;p$B{We8E-q*Xw6T-m=EMO+tO`j3+gS;)`9d!5F{*lrZHTy*wgy?-r%H& zTzO`D_zK(38m}G*WF9B_^w-t{eZ2RG?c0Mbrba}P|JeZgT3z6(bkF*+(gPgk^=I~# zP&@t}Ci1oTe@lRW*y4B>65wy(xGFVF3u21Ka}AO@)*K|cLzLLQQPPq&krjwZLPuMq z^NK#Am${2@O}klJ7rH+%+_Eh#?3M9f3eR-9zt-lkpfzf`K3c%^;#h^mo1{-O`y_8N zEfVA$ICF~rAviEPw!nR2nauM7^Hk$)^m9;&MZ+S3&zs z#T=X#POe5@a%1tfvk%WZS%t}Ae^7s`m97NV8U*+gOg5eU82VXFH1T*6%Us+kQtEp^ zv`SG$mD`I9QR%c*%rkP4II8@ncnnYVW+wEc>XVTob_hBDF&7YyAOgb*G4B=MdaRx%gYmax4^;Nm_aT1pJ?x1QB&C!HplV>2- zFqAN`p2YEBPl*&Lyo^D2WQYhq#Q;_x*fR<1L_~ z>k+)Y{B6i=k;Ed5qFX81oJM}pl=Bct=(ZU=AjgENtiYp@70P+FL1}mF-eaA#3uq)xRypoSI?`oW;YNMuj-ToEf#T=U?p#A=O0&hUyKqv z^1kqGodFDywus14z7!)*=O0#~E`Z7=ez6Udg%-kf)!@i?*-P5EcZ({wGnEZ)4rk++ zx(?YK_E|TYMWC;U1lWe-ALABZC8bl2nGFt;t7F=kxl+|1iYYW+d6YF@c(Dq&)6#f4 zeU}X-<8-VVut=3Gf&;pVH52XI_1}$%JY1s1_sInbh9sJr<`uOlp_PrJR3=d17|xtN z7E?QvroEhz$sw|`XH~Y^R2^fa{a~}PZIUp>nmUXL80B(jV z_=%{3$R)FaAQDeO@U8}wqM~TUlB#V_VwS@n6@z($?RAJACH3qR!9AuK=TX$}>q|wH z2|;U-ftbx7Q`aJ!8(d=>XGyPxi(Yw-pg^x!bMSbpFJ3C(4fhII!ad_nw0J8f*E_i( zw9yS|xguB3+cV-m)_b$InLGv&IYTgRR~8(J*SV$M=ab2W+{kOBgtIgg68Al%P-gQ7 zM0z0BH&Vn$rMKDFu5gKQ!q<92O51SyLVke3=BA!t?|+fW5^%Lc5D{I73GI3lJM7A5 z`c7D6W|6*`KvUAG@{{MH*n~@(9$h3f-IeegE4L%ag*~q*D6%U38?}C0aKN59jBX{p zVq4i73YidLdR%_jpDlbbQWLXTVRrrN)p+tTNO4MfT|g6bKO)}uszY@kJu$JNC%167vk3{56xDso`NyS4s_3e3a+D~ z3)!2J1BqYkflN^4 zt)dpjy0(8?VksUvc^!__zURCAu{OU*MT*gi(ew?akFo=Oa^B}(bv?{jxgtm{!g}kT z4%3s=j~c_1fY-sz%PaGY9EYlb9qu3l2EFcUv*^t;9}72Nkus^hJ3wTg?efOu2EO1j zU-=G7=?p~SayxpB;xzO__M3lY`|C|RQ*n-HS}A73>3(X7bNQIkN-Q3)ECDWkFFno{ z3T@S}u=(K-j7>zz%WhR+PO;zS3ymp?YTPQS#J!u+or8igxvk9{?=JOSv}I`38m zj5~b~R*Ka`wq_!pp)*A^!}yxWJVCCvmShYUQQNylPyFyZ&x}99E6*%bWn#hy2h0#QPe=eo1Meeu(}~f9|D>0$tq(r%V~NY8B?`+@nvgu?E9t$-$ZXxjY3Mu8)$=BcoVmxTLqG!CPn_NSwQxn_uYa)s zPl?@0-6}&hWg<3L?KUcs6_jboYVnwG`E!Yk0IEhkK^GEtX!}SG%U^<7PoJxT%!Mq(A5z8MhP;^|K-WN}K?V_P>)reEv-5<%>$yYvA&)G4EGn3 z6rTG=y0CaZ6aV@<^(G2Oa^Kq?j?5*7>&3(GgYi98nv$5_rK?l~Y4oLuu?tXeX>M04 z69u-MFD}M;Es^!d7z4i{1O1z(!#KVDY#-6t?XiFP-i;65Fq>GfqNrI56CR*1Ss1D}A$c~|2~Rn)pbW4je@ zIu~Xbmf!zby)h;#edq>!mAOY?`CM(Tq0}Ao-cfZAwan&s4>St*GR-{{`M~S>WOENf%rQg-;YIt_4i>aaPHa@W*Fexq^+?F-4MBnG*yt&CYhW zxpt!_GN+mF^$=7s%B4@~7gV*Xn4jdn%(bJfTX5~=mhYkxDJ1SOtzUO&uJlGyy$w}g zT;SEq3CBmnu49&C29jha4R(CUhBoAz4gDH=)n-#Iq~!irUwg}$xjLqw*Q~Su3<9yQ zQ7{vAf02=q4tOhWrRl+YG5YZBz}dKv)vnD)X3Y~$pbLrnqtoNeS&>aY!dwsu5egJs z2b=P>wA?SYHIFI1$U+T$G7aR~@m{VR)5c||$4qTTm0sLk_0q^`R_s|8$4VR<=|ipK zk|pxSA}{Scj4I8^C#D5{nwGZ-$?Ob0o|vm*aMGmbw9*()RlWG5rpPq1#_^B6T3SK@RDde`T~@x5s7)TFi8(hG76 z8w>rB$v4jL4RCP5#y@_nI0zX2_V$%~^~u1T)Wlhdd?o}MI~!!mC+d3*QNn}Z@?-4LqKH1%Gu{k^&}YwvI|KJ z$eYIRaha^J7Ynw1BqxaZagC1<8-N*aMHkM4}?_DE3V)v5F zB)qf@C1u1&-2FW%meazQMMKcJpo~VF7k|7TW%EezP;z%1g}c)rhHp2bQ3(n2u_xeB;jn7P@-V!v!!UE<+K%?)1xYCiwPVc_wLq0~pBq|!y2k9c` zEAs=woc*Q-OJ;9WwA!-9Oj!zY%N}2@a(|NhFFF`w0b4x=7!y5|v>wtyF{e2FNYdBi zf)euH3}A*IwYP58FFD-6mM)j3gpLs^LU+R|Le61B)oen;kcY;=#j@rQ zy`@u3#gT=N-85-(b!2X~ubaR7b+3I%gKu^k$0<-v8@zIlrJX z-n8#=$^OQU+$S5hKi6_q+9;~u(H29t(;5vyDomYq<*O3SL?~jZ%9REbY-9D%34PJV ze;IMIgEaw(xcn%CM~ucBSZ24ug!XXMr$kB0im*x7>y?pIx7A=m_v6NG_xo!?xBKhu z_TZBzW9>)OY;9)(wq8w{_!t8P7($U#3|YgB>u3WG9goHEma$1)w1`TSXT3Sov5JQE z4=!Hb=qmRGrO20f9hks%dT$GmBieD#>9nAQZ(Zt17?$%4Cu#j%mC zCM023CH;1kQ*}<1uWlq&EY|{L1@}#KEgUw!$0QxWCRJ#K{8zi{H&B2P@c$`rrt!YD zz1rO@azu8+orZ^hsD$_r>5bNHGHFzM(J`r&7|u|!4yA&Pr|Rf$2arP`tJ&#R ziL#}r0d@S&_8gIQYp=cvKR)F{Dx*r#>2M);Kw%*5wm(?SmVvSHX_W?FLcgykU>C zxCTa~Aj#RXx}ib>E{G4In4xtwwmGszx7m0Ypy+DW#t2!q^zKHQQ^w&MN(vguJUWDW z{kLKjHI*rxH<^nRp5>iq)@9T`41qH#i`i(x*OCQI`SF_7^5Wdw!lV-B6*(aNXi*m~Lo(;v)oW6n_I0l?cR*aV&*n_T zgc`(|Re(H(Lyw6@Hq?i%&5dZRT!W4qF#-g6D@|a^!djo@qL*ojUQX6jM>Qdt)X!F# zTF$P#g|2(ig~6kJ%$XhGaAmIvfw~)9b_n?nm(Lqf31=G8wkCp9NOZi$2W6x{rirYE z>N=Hi2$ec8X}B~h+(1@!ysxR`jI3GC@fYVmS0%V7=4@?lXK&@InQ?iS<<#hU^I!k6 zhswOOc=wxj9Gqs%c85SHOd0UJYmy{`YGeJ1s z&Ysb#{p;WP#U!e*kba0g3Py#c_tM9({Xa2MTg|u)HwhgP`r5h2z{tqmWT!YL76vz1 zedJFmKDg{vJ!_(){@qrfVftsV0wpRjjB)Slhg7Ha->Z;IM!v`&w(VVGJ{Rvutt>5= ziDbI^Pp>Sx(8%_M*F`AH4I-<#ke+8|i0|1orwdX}A*e5`e(JWSZCce=7Ss@jLOK%T zB~2&PLvnmTB5BarL=XDzY^1Era%1#mc2lI_HiJqPM0V^m!%5%+d_)6;GqAiUZ^_uV zbW8uxbg7}6*_04{?h;n>+MdqnhpSQd%01qeNrENT+DoMI;zBU9MpHtvKgAi}z9D@! zpPxR13yf=z z*q;vJqSovBF|$!$pAniI@YcHZXKfzbp(SVC{nKKD5R7J6i1@qk0q?Y{8xe?rYI1DXf`EWj#@6 zYZm8o=V|U2sD&F-2207eN990id)aoAEzx8Az4JrvYd?G&Dkxq-TU(4$Lw1OSi2vKP zO8j=}_{?LGt-R&su_@QPxcxptr8IpU%b)pWWhn~_ntP-q=A6p?6fRU|cPEdtn{G&K z1j@qB?Bb-1K+(=rdwY8pmKME~VPxZFrl^hH#6G7_Z?6y9k2}i8R;B}-CPxTJtgL?i zF3+zk{Pw5!(PuRta!H*o@%sC$7uk2D?Lwt-_ZIYp6C}e3kHFey+=|8vd~pr2r_ooW zf!s4QpO>dUsK|yL=@K$jb)aD%@hVfEsi>(*HdF=|uG}B)q^4mHe|Q>=;T$B&GCemp zg%^b*+zDV8v;YKN9hXEH71ST2;NRtj}|*h2KU!yh-YR$;!) zuKR94$0V%Po$ys_UI`{P{_NE&0DX5bL|5Kt_XmTtCrzQ$U#`17I=NhE?Dm|l1hqgv zq>1w^Y9dN77Z7ZFV}4mEjY(k{N;}SL#rG)6z^Z~>bh~(lht0uw|$xy=u6S+cB6=1uS=0USYwBBdKd+*Rdpo zKp*N3Z>F0Bi8}mQ-P(DZ;fXGbbG68OjVRlOy6Nq?_C<e0#MUAW^t)4_0xyuK zM&2qfoUNPpdwkaQ$)TW1&D$>rP>Huh-5p(TWVSP%9Hy|0VL5^Av)(#G{(??p692vR z#xMg}p0%tlKT$~4qr{MJGODag3XY<$cp#w$F5j&fD3M&wIsg9CBR~x&wh_nvMtkr= z-s;NO<$dir3si@n2u_9PqEH|@3;!s%X*9M^>ApC7I`#w$spJGzluE47&&H zGx_s)LM^@jK6``xue$x&o26%qW?ZiK)P>B&-|zM7z*uUZ+c%tb-AvxG`P2nxVUx;7 zqZ)5-a$S^QI}M(#MOgo<;5!g0vC~pNd2Q(@e|U7DRqk-G%q+$imd#;)N1Gobe$+K zsu=!haQj{5JL9BUl-O%*SvCRe=F(XzZ{q`O1PbN>XLAF@7pEPle@lxTX9BLBxICg} zd{Rli=R%psmNmDkEZz?mxYM^{~}Jix2T*j)-;t-=VVy`}K~vG-O{aRuMHZ-OKt zSa63B+@*0%a3_HTcbCT9Ap{7)-66OHm&Ubm4K(fqcXvCB)e5JMWDUh}G{U3~wcdMwz(DkpV85xBtv}n) ztTX#ALjG?(x_erWh&~OPMfxs92;+AxVYZQlVhSdT$48iyI?^^o6FnR$>UiAQ%sXGJ zr?S@}YqO(7Yhf@li0~L-h9v^K5UV@!bNA)?oQL$VVl0 zX)lLYKE3ZS7ve6|@SXY5$`W$c(2QJq^JQxlu#FpQ;{S6X`h<=0ujupp_f9%_s{g`0 zf|dW11O*1{pIBg6|7%e^x&JA?0&J-io=W`xwg3D0|JOZZ%)!a29;mOUjvH_MeDB;6 z_D>SUCBckCh83TZjV%g@pIM_@2bH&!ffct;ss?`>uK@9w9xVeyh%JxYlM6B0VJXAE zOwGG@K^dZaaf8;itImzb*Pb~!Im_qn$F2g?`_5*_Nq-|1@Xy+jZLb81TeFV6gF~z1 z5sLx?toOf%19YI4@W01T@5*kZ!hesTD2kx}J8b^P&XBcotkTjh-3AijH(~dAe?u4m zoUy+STr1ZX7yV|*RRQj9Y&sq0$SYZt4%Qw0Pr+hut$2GRG1p+;>!Q5HZmfvAbh7(< z4DZ`~^u(6x*FNBXg#`fAK?xKi>%WuwsgL~2l-U4!!(K!mQmA&sGa`78_L!m};RSa3 z&jh&cetNV6E`@}mk>#3%@6)nn`zQXOdgZ0HTvcJC*&p=IW*RdO;_wLf@*Er-W7EH- z3|H}FyeNhq-bPJx{hKS8wf~NjfEPeVC8KY%w^cGVfVm`Z9AJh?AieN+{m=BUjeU!YOU_qE$!TTyZKUpq2eyHBBW+bN^uI zi$MMAK(EkiT`n7*Tb={sRp}{SX~ce0%1k<9S7}sI3dV`AFXq zN_v)b1V7BPD^@C04^LMy^hbj;Mc6ZDWbyX98_x6o-LD88)Pc`mWV{rOH#cytlse$~ z7Q-wUfkbh#^~QrXibBqI7tA%7-r;acU+9RMrT6<-%Tof6kD8CofZ;I2hd*~<^LV?M zs(406Hct45KIoOC)f#q~-cg~j9vH9ptdk4-sh4>xXKb3!C+9RJarSh#rgxGHmHp?n%NRHo7g2cWkKy{TmjFnW8fs-DF_gEku%F0X| zFZPZw9}#MI7-@#Ci4&=5H-%N6?1SeZaV1omGa8j8HQ<@vz;jKtDo7}9(91?{93Ux; zkdK;A9oHY#{X?Ys2UC^qW?B!~#!9V_VRt3H^9!tPj-T8!eb&*$6=5y?D4e6^#uyAH ziktT?`9~L;F>Z(1BZ+LLLLs;;CZk!`vDG=tB(At#4&>UR`%fWojrOVcMR5aZ(g5KP zvni$5RJ-bBCnYD&i2iVM z?NM3&()#uc)!tc3;xGbcvkoREmL29h!B%t<$&8_Lf!?QGBhVxX`lmmQ4*4zqqg#MA~ zr#Ac@km~K})9obty@cL=OI-8kHEMeEsrZBYX8w3C)te*LKUPy|53Tp#i4=F*=-hyB zi?Qi~X+zN~kUt}_FM1#KbAaHobx+$U#kS^n9HWQthWsHcl{c{yM*8Pw1?}I- zD#?W?YCnyv%4Bk_{J7EQvU%FxuT<3WJC)CmoF;uB!kQ<*@HNal;f;?&W|n2T>FBRZ zyA?yt8Kt?X4_L$NlegYO!6a>Xn)UKVw?T3~TZ#gAizX)4;EqZSv)5v;BnK1mx8D<} z#RpT6#|`Qy*lqBd+O72Y<0l_NEhAb@-z|}Y5ZVzvudZMMj+eg%i%^w`6(?SJye%nL1}0vM1}%1MiI~$q^@q=5f)~VX0#9QD5ZiR zXD?gQ^t5h!o~?5>Hy5m-D?XkO%0C*yLKmD%dz|I#GE-@-M_4n;;s=?{8h}Dy2bNVI z;a$lM9U@Y}!H*z6x5xE~OOw;J9;(mr)|6g<%6Ko5myTi2BXMn{pO{}MV%T?bUXf{A z>ROsAFG6(_Ce)hqah1cIiu&l1TqcfhTCS%jy$WF&3;jEjLwfsP(UZRHy!&INXp-fZ zuMe7ofK6a|^14hRdB>xThbMaj$m*+#9dm{ph6>4@=*+?)u%NS_Z-q1N%9PzVMi=mm zS9dta#nf=LXYBcf{$ls{_ZdHa>nAx zU`$EQl%ci*TLTC8SH-R=h>y+jL_V%Ks#v)fn;B)LS$s1uwIlS8q!aAt$Y|JF z8ZT4!4@?ff{yJRO;^GTnTlh(b@{0>>C~5o_lGnRJj_KE}0k`sOC=eCKDkW_j9liAq zQ}dD*Kv@F^WJR*jGr0W_5=eR;;5h#dKYBX+Xo^ty(C{;XLu%>;Tf_vHu*h}x!ZHoT z{j8VQBlm~LoiF*1M2VejQC?jmaUB?$8dKrm;CL|dJAW>R9p>i3(Cbd}EU$;+vbW<< zf(+2v8e!s7%=>-)GSryCA2qc@TI;DG$-Vk(W)b0ka}a2u0M!S_E49ZO+XBQeH@wtt zq~_DDjUo!Qnox0=?rIo!{_#zRj5EUEbf6^3KxO&htBZLZM?yE!80Na~1I*3eWSOrG z>JtIC-NX>)#7=QM?ZGZQ`9FV@C0QFR3Dc+Hz2yUOsMz{K2KL)Im^w1wm{96;>-1vK zusxqwAa>4na$z2TOTlU!@Q31(MO6;%x35;NlzRyY&X@Ds*0rC81-^N@YwC>x_;lh5 zEna_o$5XPpozSRM^+=JgG|z1of{O2!r5{crwmmCLe*~KCa_vd(PCo;#oCE#qkQIME z8vAS>KHhO(K3)nPG+!>3#oj*L(gHp`TnG1kA0^gEZqju*W*c~#_3?r9KxlTkzm+<* z38fSdHYGj^CQ9IUG5JAl7S@3d_G~cKR6~1??VOX=6@;l)`zHVP*P9+~& zDVG&>q}i>^|NOhu$o^Z%Q=M@Q zS@YnpOX2=sDPOUJqNPUq^G~NO>$Wq(=UtdNu@?5ywo$nyc+s{ue?(UVZv1kuBOdTwDQOPsX z%Xe&AjZE*BZPp<*6D&U~$kab)6iRtu1I5Qx<`xA+>i2Xi7-(?wT@tq=ZDw@*X0}!` zKu3GSm~TI0+9Z4q!nLL~_3*+UU}a?@Qn2At*A8S~`?gBJUjz!!wVWzoeb^sR=eY&N zGc9#)?0@;p1^I38)m&X~nl%i;Q+;83(gwRH%?(bJZ<;sK3i0cN=FHX2p_|nQM3Ag! z-9DfaeJ4YmDiobbT5f3Yg~@dBjLwtGhv`@C#3wu_z2r|QlJ5@2haJi61bT=;v4*de zXC5O=;hz5xc26Q;Y3hB+>D<4=Kg#BVpYU)l@WS{;YUxXjJ+61xk#abt@IcE?EsyA&;o7!o&+H=|%bK2r~QD(+$eET&biO-Z= zmOJ*74#_(UU-P7CVrDIZg{^tRSN@WrMjZucM%@tMH$%JX!VWL0a7*>CyY2gTG+rg) z#_Q|jbA^6$9INDy(VdR)Ta|$2YvQKOZv055s-jCjrgZW7ICRthN+(~u+Js_`VVACi zo2{5OeNZ8%_E0JvkNAaNlAgG^0=~$lU(ab(N?_EX(>9CD)5wt|*q5P}2d4nKC}d#U7qUbfiCC`)xYi3Dz_NCaM<+hz9j<|5Hg?6&?JW>et^VFC^J}}bt=|w40qa{tnyw3 z6w%9aDvN$3&)OG36*|(j?3*QaYYxNc!&xoR`qj*gDFm&@%guTX-cEp3$g}v@&cXy%4^V-UO$J>^_FO! z4|~YS2BXnR<(Fce%j@&frKY$)e=wc*iu0J8X$sZrv2$yLII6&;b3)#}J}phY*RoTU z*7p)zd!fIHA**(TK$bCc*527#Yq*GWSEI~ppKr2ct}V}i`#cyi)mekQp7dWrC@TAD zF{7W3u{YjMLI0jv&x3|IOTJWovJLRjI(Z@8x>mc|5ak@p;vYV)r93Gfwk_C zK(fT&_`-94F0m@?9CY3bBIq4sq6F;96sqwBh_8Q=Ngk7u9Wq`n%pYT_4_`i;yS{C< zIa#~iogsDn+9PQXIGp!MyJBh8g__(=q^KYF4Sa^(`{qq}yV8AuCDgp1(n#zsA25yrvtle8({teRRNQ zYL6*Ril;E3s5~R1!l(u<4mi)b*#B^=P?&S*kYCI?v5Q+_H!3nI+060nw0CzrsinL$&N$NOjGHR+j<+u{j7zO3G}F5r$iB zLa8%8msi;kH@#0;4+a+T%|jGEk9dzB7gmd!%6;KLagi5gzSm024d(H0k)IauDG4mg z*_ZK^&~yc*Pb_?HR(0U+^o$G~a;3)L9|V_nBdzIwmK41^zy3+vJJMX1w(#=uTA8^P za1M=6;~0-siswqcAZY}ZI%rm_)CSD{$@NqLXEX0m@l(XDA@D1B%$czbbZp49t;$k3 zT~-}O{gG|!-IKl4f#1e54jrWa==q+PkV!6Z%tqZz} z=X#4U(Vv?VB8r<+5>tg(dQsu;2&cxp#zyf<5viey9rB5r(S!pRDpI${Nn|YQ?}V5h z4~iawvZp^NjY=s9T*4{fa|f;lo$Kjc8r`j@6c!fdkJ=m72AP$b<>l1{U#z}dEld?Z z2KoGHDg1S9<{!MhEz%03vAaW}vG4Eo3*C#6q~|v;n3pkb6UQc!U=@RQ8)&7SN;B_B zr8irxjgpENEL0~_m9_Mn67AC3d@C0WyI2TfH`{ZiPG$GAgG#WQoV|H*c_^n-o|-oc zpyF-Y)fh!ZZMrQu*_&!(RakwC1Fv$WlyPuQbqyZ!_$F06>}JRCIZ8omunKaGEaAzaKo;t~8O<*X`*OER0`1t!gioS? zI*-MK*5q*V&;XT?AiBIvZAc<8WYW?n^-6@QA| zG|Jw!;Wpb13UdTC_dY!n!_BO)5`-q3cJ=&Bpx3L@s|wSYwM( zdN#9DS_&>elU^spt*x7eLYlQPfsxV$UE=nm_%Db(FikUf;@K!9+3dI82cm9?^kCy% zG z#|XiXeR3CgGU#XM6UzUYOat2MZi|Jt9D;0nu1YN=+!?ihGAeSu6#XJSkGCF z(06T3Kii!N`)ZxmH*7AMH`l46DT2jCxO0zS$-q{r1=c-#?y!{B%^$bWN96bpPsf&z zCd2t%4+>G+??uEj+C7<|riQ;F!5~D(i*?b=a-NzbLF#-@5wvtn1Z$HxnZz#~ia}in= zeZL{19U+vyY(lv)I&fP#8S&3_pPZszw#RqXngHuI`bFeTMG5WkP;1$9<)^#C(S(u? zmOpQ%rKivNDa+62)caFgBQ|j7Sr}>6He4~^+^=}_b-~}kcQD_ z54_pU_0baT*E8uR^p^LZ zm}mQ&&N*FExdiSNOxqGy>-EfV3(d`QjB{e*(#(jErKErqa^=yV>0J_}!`ED{R#fotRL? zw45j7a+M;N{My9{c$^iHLcf0L-!s)&S(q}Z>WY6a%MoM^RoMK|y41an^#gyFH)ZgQ zV{A-GT7Kj$6GH^rg8QiHv|2Z@3tZ0ySW&+S)o?18UuzmmSxP~cLXauo>@$lre_oZN z6%vR$s9fCN2wfoQIZb*;pCAla?Z35$6%g0l=YVwLjMlyduCf?A=QN@rvomh z{r(Xr%l*%C&(GC@7i=#Y;xiuh;7SEcd>Na!)+5=T`@b)Li74J<3zN^@f|(!QE*YZ} zcXaNuWGkAy*!E-n1k2Kw+Oh!MY9$|JXapZ#G698nIXB9E?$iu$Oe9~ zQ5428qS-D1qr{)n<7TNxgz{YXaI<=w326&@sE=%foVbr|=O8CWrphS36BGHgH^BAr zlS*IrBpc{fA3&L?S!PIH+O_X<+xBk_JN_tvXi`JaOdXE<0#bSB#XH&C$_t7MO;=9r zlT)%QP+O89oK6~#u=p^>*CIxj^_oA;1=sm^r!_3~lBuI~40{Qj z;7qIiLs`hqf1$ubk-ByA?kjH51*`_BydHF276_j*M!^(26a$~gHCm&F4u#r@Y!p}b z>vyNMh<*;kG7cz4md6GZH^)CAI*v$n8hbkB`1ej3KwgMKzOFbDaD@jWN*nEEQ&=nR zi8T84u>SO7C&7A`NG*2H^i7FBn$;FAALUS@ELKnv2G_RHwP1yUn8rT_!)rI0{^?B9 zo@N{yzyw!u?ozcXa~2gf6^>mN|pZ`d$K;iS6>(dVt&;xc%4|6(l$tj=S_mAsQY-D1sf&XhU*VrdW$ z69dXREocE@BA%rffFmqiY>8L5VK%fB7ep;gV?Bx3%)yed)mj2{eJa8XrZJ_#Qz@f{ zx5}|gXbi+{+qwgg{oG>O0I4~V={EsVBoBItc^~I`su@sK4fv8u{jZi6dr}tgM9W#0 z;GaY&0TGK0U!PWNUV$VrIAdT*Bt2ZXB-2T)O%s2h^lXu{zexl5POUaYX2zc>E@Xm~ zQDyBvB%X(iulR4X=8%#2dTqk>i7hN_&9sYdqQn%K>nBl{9*7%-3x^BQg*S)eTCBrm z6OmNHwkH-ltwH5x?+hlCVFnmHIcaWqFZRt;m)|nsF@oSq+3)-`EbiWL8t(ce87Hfm zV5_!%swVRyrofbMs)#edk4Id%iJmAyCy~R77bus8nD&)Ba%Y%ID) zE)C~rCZCi@5GjB=#vW^Xt&uI~7wUtg_d%(l7uR(|tX!&sxj0{nb(cgq8g}!m{RA;; zVSYU`g!9uy{bq|ggEt$!5B^W8%otJrw!W?XKZr{$XuZ!#b?Og4kg8$jRaYPd)gp)M zaG%6vWI$2#ov96%e0cD#HRJ{mnAr<{ZoU-bDf63!}&_k|kO^EI~rz2T7Uf)Y&`D-H|t%cIb?9 zattD6ci|`pagCNMsrRSV6m)_?rM@p&MKR34)Wl)--)UmJ1Sg0J=#~aOgTj){VlVdR zK%N~uvo*qzn@inMaBgl!*U^0txzxb4=##YKg{H9HgHERBP2y`gw_AzWFQ9?HPLv_c zo*}n+s#Set4sw6r-RAvM;xX%dcZU5ML&w8|&xUDUPFnPPcjpI3GB-3;EiXb~rig4B z^WKFFTc!8D>Y&`QlxzH#l57r!mys3(-8gBf|nOK2{ zIU+zGUrhyJ;58PS=3aV$Bdf>|hiYu4!+^|WY2rXWNNDYTN@sZ@pLAvP_HaF^exkU@ zZWXX0pUop8=zg##MINx%Qzxh%k;!#PH}pZ(An;x3UU7`lQv;shC-_w5G^-50KTI@M z6YJ+v)`r9prhFJ*Z!end4M!(isqi3Mez-jpZac9#cs%?pq@kiPzWyxb11o-Bef>Hz zY?@flMzJh20EH+m2wYiY+zxPYZ(^iR*2lb;d5L`{DXr7T)8Lji`H@J%TIxW{D{HpRrJ@6! zmGkd$`-Es(yh?fQ#BuXxa9#1a7(p>&5ec!_s(D{hGJhGfus1b+8)Rre?b zOTbwULRL*ztYUM75${ zO3gD{y53rlzF)3ZT{>GN4rIiF-Uzsx-;Z9DLuZ84-^ZF!ik0UEy#E=XM=NHgi2Tx+ zRCEN(dr+{ICtlgzwPJ!3i+$g+RVpC>`aGZ8hH+--mvBqe*tO(P04LuKu0K6Uhu6UfHqK%#U!AcD8qnz_C~__k%s> zg8m#O`w0PpOGVJ|x=UN<2SZU*zj$ht6jWm7@89fZLR^#y$IeDJ>*P+4gO2!Fl=GWqCv*g3D&%?3JftNXTM(MA-QgN(-rwcoy$BQG)g5H&aK&u$CwJ?uI zsOT?>SdN#+lum7nr|#BQ$Mtttd!=^VUV|xVvOT5ZY6|GZ>GH79zNS{FgcG>SW&thq z#JuTR+BaageV2{4|4_`?#Neqv0{n(NZ`v%IPI`O5r<8BHr8N|MUZg(0^!9i>6}apl z0BUNo1ulQ<*6&{E9%uh5TNzXCw!K>xxWP4vNz?1GA|2!?%$~=a9InjzG~Y+ChI^iW z+2(ylz!T{wiUvV3SdMH**()7R?pNLO3t$~K}gc-K7c z5ajtUa!^8dYQWz6Di23nGy*L@=`LP9Z9d-VW&$N49;10-VE0vT9XCtDGA8EC;>_Sc zV5uSSCY{puq0dLz7t7DNn)4crpGygS==r_G+J_onZU{rsX}m;x0E8{)Z=-)V(r~s&)vbF=V!wg7*=M~A8P{6m4)?2_o#}IKfJx*X(V>jI**-C|byG#XufO0CDl;~iQ`PRpH}$9sn=aavc$ zQUb?vl!3OWIlu9}3s)xO{2>WZc_vL2tDI6ce9e1)y=57eoRM!KJezqLrzw^rMg~j7 zC!T2FVFcn`(@!n-&mAm&nxljIfhsU|6h`RPnAG0@5iAVUwXv#5_IaS6PPQJdXL9O8 zIK+?K9V+m&duq7TdYCr5m(D6!P9x{7U&S0eFeT`+rm_Xg^Q0<>w7+T(q`pHP;_&&^ zrKl_#Y4#H_Xh1&Z5bLNaCKhLu3W4Sm^noG~YIie#Bbz5hP zsr+MM=IZ#ROyf7TRVG+hng&GG6*KKucPoTTGher!Mz}rCXxXj3T4geK)p4AM>vheg zo0}aS{|=G49I;r>3Z6Y|DBoeWT*2{-?~lP{MKh>;M)}B@xN9rKdL?k~p+CdhO^{pH z4hb*SgB&mYktEiEW%;G*<2SPV6SN2Cxte(%a~|ld+c7WL>ejmNvI_-BqLxt;qSeTM zVwbk%Kdv*dJor!$x|N51os$#%B@AtktGw$Lp7WV0`j|t*m@xnQ=$`NyHMxNeZZ{uu z99c_4B2V~?GJ7xIy=Cc4*&YGo zvbk*R${XU5t6$zj5#Ej@!VMSnrFAFtr47KRWoO5xM9XRY3-F24a$1nra#=;aJNaQp ztQo6dzqj6Z*-=iwlg5k!MU%iUcJJ@EC@h#BnZ~V($_qPHKU=&Vq_fGQ-08Hg-gsv> z#Oh1TKI>w*RxTGEBlu=Ec%sfXBHyQb91&;FOz6Azb7&aC4#UhRte^toC}g|0vOiWnW1DLnnYiW;(JLs0r0>sJ zey0UaZ$KX~JoSO{0HiSDc(V0iV`YMzi!QPKf7{7Lv4)6MaWgB8tSD0>f_8fyA1h z?;tw~6P|;iwYuvpmrPk%4TupmEj{a9{adqPSj<9E^TizCPk^7=$K#EqM8G6*Ubr>5 z2>zAbv#*+_<$wGo>upYOH*Ct%UDps$- zf+sSm_I1=Xy>W!M5dkkieh%!v2m2xQdedW>%=t z{wM#T@06u=WMF`ypupxQd8V+ND9hSLisDMOep;{W_sea=TLa%AzlD{~?K`v(Xt?$v zWe^DkZ`v6-#a3#hT)42<_>VU_w_F)Q9>DH%6V!Cl#~u%+&2>G+mEkHgm>H~fUG;G} zN@x{mYt^0WKIs-ZJ4k!~!8FDUo1?M7X)uZ1lwl56EbnCc6Mypbk`|(cYU#Rnlg8|zL;QgGy|b7 zqNsTEWn{hQI8bXJ+a5VOdc03wx<3~>@IKJa8_^v}cIpB%EXo{j7(hysyXNU4j(c?< zIv$0zM)vHn7{)}(r+6xF!}LDWQ|_DBB@ChQ3Yul4%RYX@j~EL6R1;b>6LES-lYcT? z%n4p-NU|crJMDW});svC@hcfQy(A&tLPH&u_qt!m%RfnWl09DtNmB{0 z0hKpwRZ%$Mm%^dMryHT#Pnc0GGEz`wj_@e22DV~IL>HgML|Dg zg#*R1Cb8&ffvbV3@|9*u*S(b>x8+~}?XKx-I=?W=lS}Vd>x)S`*>Y(tr!BI$DWQJf})D_1LJ*R7?+tO$|*2c7* z+gC%?G}E{h8WFx^A8ES7J!}Fc(b+V2RIwDMDmS5tzI8^azCJ<2wN1`k|B&o9k@Og(RF^Q}ntT2ACBwi5mk4z9$bCGSIY%JnXl z;8xViJ50OfBg$y&vMlrcm!5Zng7ZF03-jlPKDL+5m-bW8vZHyA5Oqi{DRh;3G)7cI z1}_lX__q+nM)ouuaKeU%9#FR@$jIc7J~j||9-Z6^I6k)4?H>5pUkbXMjz}&o$BjTf z5?&Zch*Clh#d|;xZA+!ZLp4y$R?m5sis$>=Y(~%Wb#%l;D*g4w1D;PqyaxBU$^K+K z)}=HaxP4VMEjHC|CK~uA$f)|Sw3P=)l_#>6)6`IGUg_GlQweWg>@u&Y3JV9s1iHMN zGM+kP<7HGEWnJ0OAlyxP#WAI%IrrXyRpjcKBup({Iw#j}Bk-+LO%2D@t{E8Zjlg#d z-qFa6_lB0YdIjM(fhHPRxT9s&+-%IK|CW)ReTxY*R$FlmoC}E3>=n9e6u>P8<3`C+sm&9 zGksBcXCk54;0#!ruw@gl{)|+xw(ZukwiEG3?~B;)X3X8E1%fWBk(tj8q$nGvPM=xVJ>4Q!x(x8D$xJQw7Be+#6 z8yMgg&XDx(j)iq}z`bs8$uBSXPU__WbNP)Q7?Ixl4NGxjT)D?#&v$qYkW2sW6$2t( z9(+sFirn0|9-X>HPRAKjy%)abG@S2o3HKVgQTgLXsgXs0d{%qLY-)wIGuX(=+GxO8 z|HU%o@X*w3^=xIBF~=oAtP*oUPqt%9C}Vh`LHYBvzjXjSzWbgXG06HYw9(bh@&nu7 zl{tKLeB6j2u!4VW1-V^uZz|6mu(-)HKVTj;i`8TOKDA55&3!5R67QO?7&1lR?6Ir6 zVaV{^QxSl_0d@ayk#RV9_@IG+1?$cCH!A?EArAOZe|i`H9571V?py(DfA(s1L(r3e zCa%08hqYCURa3w0z+>hq79eKVt!8Lu{wSjNY9eu>ryn2EJPc0uHrZ!y_4 z%i!YIVEcLxmAH0Geb0 z97WbAtW$=|+VdCqfv71%;X@`+Yi`yHr!dd!I+Jlq3v~~CFJrxFK$M>g4D`WB^N9aJ z@Wjn*xJ0m67F+g+hVBysZ#u^r@^m^I1?ZZmd=Jp1QpXZ)fsTG^|4(+!956^B#+mfF zp@XHnes$s^8ba&go>iG+r%=la@Mxoa5uVhxey`1gZiMlV1B;>n3VJYj={;|bpm)1B zyVv+;KkhfCVr_SHc{htv-1b7n=Fw2g|DqyM*%9u1K=Gn26=h^{s+_YspE)n@nx}ts zS`r`$Mn6-i6TLu4_s@bsk|hiuGb&re%A9Uar8K9T_0=4BiS``Ah{afLiWd!Qp%APB zPgl#={(T|hfd7MJEVMb1{4W3qgZJzISPbhaS@YEX|BZ0|iv)hg>T2hD z=xKEU&__a)^|iIV2X9JDa?Id2XhFcz0?fAnv8VrjYC%z=xc{4boc?d_RSb=6$>JYM z^Y<|ppq%pW4*(EG{{Jxs>Hha$v->X_g{OI^^xr@Ge;T8D3 zv^}$Su_u!ETmr25pWnz20NvrP@}C(qhWhJ~)xM}eC+USzpJQptofwMC6SruwH=ye8 z1sf8~Q>2Bms76tw;e30~%V08Mx;{%XKJCy;#v7k5zQ6q^xTJ|pg#^bkzEXdc>xauY z3?G8QMlqeL1D^3B;Q8t6cfDQrvG*^FWlss5TkJb4S+nK8S>Upsw_YC4uf|-Lx62|s z&!fRpn6xC~pkh=;ms4GAgl0S>BGZrz)LjT+YQIn*s=fyay9Q?JEwk#saDJon_~P>6 z9?)w2?T`WUuXi@;>F}P^(`I|?mqpf8Fs>exRR0*7z^Wi zQjOzKt9y|Du}u~he{`%s)UJmwne{SaBxG*CbKQBL86t&{MP7bOrwNI4N};D4*i+Q` z5yMz34Xovag43AomAvI-)0*Es2kwl2sbBoDYGOC#m$f9RThNzbh_n##-+}05 z1r1Q|{cazN9PFJ&E{dzhr6rw|gGVy({e$*)%mSA3yBqAOCncYj!cIG*lUNQTz4pevj&}uK?_dx}0 z{7i(SByDilWuY77z@T0Y?X*Tj3Y34R%YY5X$_i!TiMH=0_5cajZE=()s!8>H<}RHU z4`1^Ipbi{V%#&NBZ&?Uwg_Kl0#|;-g!YQ@GhFw@wMLNkR0uikb z_6YU2gbP@UBKO_3b^9lhu0D2bUuFuY({xT=5Rd#cR9p}BCUBIq$bu9l&mhaYlYx8< zBlgAnTf*JMD%&`MCB7oSc_+g=6Emat6(Z>(Y5pa2!e1oIP}_gAn7JePWvZw@Yau+5 zrkJywgM4*<>}J$x<*{8bXP2AvJG;$jwTJ|)qbfbV`WDk_u62=>6|KQn-^H-Xuf&2^ zmYcgOhc6|{zpB@k-+<*knlAzPr`@eUyn+I2e1g?4 z?n*Z&f(b{HtK0;KZeBxzr1j}dLQ$1IIEdE~zT)6qd$NWwQlU9znkRpPwli<5KsKIW z!&=l_DFq219=n+tMnD?iU$cKJuo2Y*m-YK|*RXum)gCnedI_|vUcaC)5|wsL>O`LS zcUhG{>2t|+X8N+baV%jc+;F)&T+!?qBrRDPIOejX=avHs3&7$j3$hb)x+xOAp;6AL z^PdxDrzN}7RH*-x-{d&l0!gROH(8g~MM18I?=I*dpNI~#9R@wr54nI*B zwNnFf)4##xx}F<#1JZ2|rAurTrt~D2PP`{yPv#O)8s_?&i2KQe~!gCE!5%NY(nt+FNO5J?pYF zFg~&XZ=blMnfw<;J?#25ht9f1;zivNn_J`DgI;YT-oMw66qK9vYhb>*L2M@7K zpZHA{pYfdw#$+tIvzej}0!-@T`)&t4~jM*hizOf?H8sK9Kq` ziw2*oJbklBA$NBwFlkjn`Rn%mtnRU}w~Mt8_4yU#9TIZ+^uuU&lg}lKt6p5#7aWr9 z@pCoryS+nRCq1<53i)u2ur`4Qm+d9ji~0z)^yCks8`r5`qlkC6ian9}7=Np8?6JoV z>rVEOI5E;4@K}T{8CyN;W0Pw}SnuOZ>+c@`sqOF+C)Jwk4Yy3~I9Lfr@H;=XM8fVA2fhlOD(w+PD(mImV*ffp~gqRah9mrCE6; zx$#^7kg?v8?jL zdD0wXeD32v&z+1Phrbp=1D@mKLm;L{0X5Z6;}Re!mkNYwD_H`e{>`@pp?oV~Z&xMZ z8wgMss38XpymUQJ;&xs4UuP^eBR?WRCHZl~P@r+oM%Hq~QKp#IGpNB$?!wQ9&XdHJ zuiHnJJvS;0f=sLo0cafPhhPaslAr{;#kd(@QV&gJ(`C#i0GV)51fd&@*R2w89xIi$ zTp0}$C10NBM|80czG)ff5YS=CGsgs*lVhTFsl?n@%$3*fZ$KkVAMR0g>-0qn(_(chKdu6$EfYD9c(|Iu}JJx$7&L$~nFw@kbqwKLa zEZZJGe)}Ch>I%JEcx}3O{G*2}U5Z9lV#N?zJ#JA#3-qN=b6C*Eo}P7D7-cPTgH}f; zGRr1}>^!?|Q{Y5GQjJ=L@_Skgm}o5R4eF@G#bFhh0x9XX&s{~^ug(S^t- zWj)J$tt9ovKc)Wi^Wh?1#shBBg9YJIeK4>?B7fA0M(lYAZ`Bd>(+nR7F1Zmb%y*w4 zoCK>4+(hq>kH0W`o%F4E@=bRom`9*52QrXv`<~mq~MQ5-I)Q|M? zp3;%<{bXbnPrmMJHzD6=HGoL8%`xGI^(9t#Ad*{%)c$zFz>6>CZiZ}!gUmhKtmW6T zX0+z^;bL3Z+ zTr7V@()an*V$rjCDFN^b(d><^XJ1dZmiieLYz`+X-CE_3vuFLYxzl zWJE@RZ{&`tMiZpl;tpl=f=MsH?9e1!X}TB^%Dop%e#jP0kK(_@W~-PD6ecJY6DX`- zb3TwhY!Rf}iB@PJVYMh&gnU&beT+OG`mnR0m;aTMj9DK#i&-RS4OeRI$h0-Qg+X#u zBW@CJjDcDcsl*!HaU6rcgfwL8TmQg`~-_Q)q*v5AoU{bbiUD__|OX z2*WkLSr5%ysRz_fwit#%sQ_#e&_K!A||6Ib349> zV!KO=zLcqgW^JHr-|2a>o+GA4=AY=5rcg3--}?`e5BSqIse_)btG&aN6>E9TbUJL5 zsclNe13i?dEu^TE{nv_~1*_3##lAajGEW1mpe73dL&ddgw#%5I9BRbaC(+SF=XNWj!n z4mMZ0@WM~{tE1?N5S(S_!IG)V;Mrzu5_Gi3@)lyN0sT zviBO1X+crJ+sFa1bH}k`WZ0>e4B|3_;y`WR8BUF%q@`<%3IfnMtoWt`C)&+l`bYC80{gCv zB`bs$whPwgXT2X^$A3C<`Q?&SV>;!LG;$Xch@MnsbmUV1nddhd52tk*Lg+w^tZyT7 z+a|-YF*n8cnDRzpcN20jHw0~}IK2*aJ@t_Q(kl^Sg6VE>PTh3)SkNH=oYyE8-!Ki2 zzs3$$he*;mk@@m7K3vK90AHMS@Ua6I#B1$Q7e6*so6ZX*jie3sJ8INaH^znCJyS{d zGezi5Z4TSX9L}4k%;u^h+HXjdPLic6YLPn7{~;Hv359Wkt^-nir*hz%^u*r^oMc?)W72qABr@zhUM}9f3o%2;~>v#8rm#--| z0J1rX>!y4^--w4W8=#OD-)*rAodq-VouhVaxMFn-X`}Ph)XhkTLxKhQQxmpK;^O1w z6cxJ<0PF0!-6KUlD;Vlo4TnZheV}zf&(3eDcQ0ZS6233mc-^+LTE5M?eAdx+PgCA}b$!3SZlR#} zRoX(B?@*2ozHsdP@%11Rlbc1M`=-o@$DQl_R-Y)W#8bpT?inVC--G#6FSOxlq^+YZ z2)!Im>ni*}O@nE-Mf3$~4~J@t2dc_Ff6f>f2~y$>2@og=$pV7d>M9!;4*qeGU*!7@zC| zzn&CAP97PnyIBKfI))ETwNqLk=(>HJhMz=en|*vw&?XFUw<%Z0 zFO&(+U}*yTq%UHoV_l>mL_974+8_+mx$lnvqPZ-+CX{w_`h%e~L~I%^Lf2tvh87#4 z)aY==q)>m=ZlgMi!&9a=nvVW1^d%CV6Y&S>6HY*xN8lx8Tc7b1Z)MetvILbZ9}(5> z$%xe%zkC-YzVm?cd{b8%Ci*ODi6CGuRpR~Dv&COX+;Aj5oH!VslEKZX3YhyFwl^HZ z;KS47#ffNu7*Y`Mo0sitf&Aml4auJbLr|dmn0gZ7Y%GGL_2*pj?&^5dUe`4;0{ATc zgBrGml8Ad-Fe0(TR)BHQUL$dWC~^bK9Y*J3+R21V#A!oE3%|(oeewhB@}?kUdz>cI z&(5Zn=uSRL?kCk4PoUoKB)XRtmliz!&60f9G}Lmmxe8=Eu#w|wv0rS&rdP7Nra^wq zIw|7(?YD@t_nrE80_NjmHhXF`m^Eq+k> z?q#L?3IVSSaguSU0Zo?Ow(g5S>({{X~1$(FW zRVCF?^>%NHy@&an#e5yWJf-FdZhWCjIeA1pK5z46K${i^be~tH2W%LvP9#*GI7j#m zNj7DoK^GxhLqpPB)mC(y3`(X}^L8kp@8k2j1t9MB!3{#n9d_5&zTC(^pLv~dB0S_M zoX8N}D$rGeeFO{X;BK`T;3!!pX^}=%Urv5N$z)(pfjqAN6pSUM_;07wCqi_XZ;18o z1Ca*L17WUzROa!heyboNKFQNv7wrlQ;}f&wKw&j4o+FHSSksfOdl-7{=c__jL$6mX zE^%CURQa6P;+2|XEZNBhcge0*NM9Wl80_ZtLY}*Hz~5@8VF)-da9QmdNJpj*(y~m}L;VNWCDH@gok^aP!8&O@&C7py}^g)h? zScGxRyXzD1Gy8UGub@_qu`&m@IU7U-55R9<0E%C}kj_kMw)^$F7R6+)Cfgei6LI;x z1A;~jXD%!hh(&*>AheuD=1daulduIqNFN$94WD^V9rMW5J6?$Nb)LTAyG;Krnk0OB z)sC?1%$9WclQZwQ6^0uAM|##8LZl!p)42eqh%ABNc<8vv&`nhYxky6w`onH|vYg8| zuJSDttHFYVButa5LshN}Ht-l7*Xuh~Ri3and2ltJC7GRtqiL%p#qdR`WKTK?#t|q& zy0g}(LQBkV^^$f7F+L7-v_XJeUp$giFzWF7w7JG2t<0Yf3%QabzO| zdkF#iu#7%sd4-)-4~~seGcqRsc%&s303mXWZ1X{`2t$p!^-~XvV#RJtc~EyKl5E2w zCfVm>N`RE2>RFuJSmjS|NB)!9LL4^1a9c09r`P!cg5us?!v{Z1jX2SkEQe&D1mtX) z>`XGV2QMwLaJ7!|rSrgcH@@v7CLL%i-eXCF&wj|R5HLv3$Q}%C)p0nl9voGs?mg*g z+PQ@P_NRVEx{?5xtZr#YO-%GFvj=_&Uj=ywOBqw8UyQr~-(1G0Om|?>yEiSvN5>fi z1takq$de5#Ps;M_naQmUXSTD3Z&&gPrO;(`{8k2pcuYoT|TNtaNDjVwB(bAmd))-5Ey*xmx+&=Um=w+apa*Q*H zn$?HV(UfY*E}968Chdw5K}5xaVl$FuTzn|+02IT!#r=-^KU5NKCI?&u_jubA=z@`o zresu=FjH^&x3=Mchk6w%wZEdlywYflV(jmjBeU??Q#hhRRpLMp7`;&NU-XjB-n-h;CUzVFE6kBG<>M=xEbz8NkQtQ> ze545{%wv+yG&Il8GVY<2!&;5NImhkj-%5YvpP)^GCmXBG`}69nk^?FMZ5J=vJudfj znhiJ#%7qDQg*6h5qad4kt7sZr2MIO&7EWy5lkuk>%Z<9rYBh^T@b|&!X5ToAjmM4e zLj1_GaJvkFA$3+oc_5_pudEALhP#C6Q*uAQF@CXa@9Dns$yaKL$nt&?QWCa#_E6(} z<0W?!cL(%Z(u8L6C9M?8-B{N@tzac1B}4jF(4GSzot1+2jXIRA9WH_{a9^Y`We*Y3 zAV0$z!OW7F})rY1*W3Lp!s zV-^7_b;A6Nj}W#~>t`2cr8>+1WqnJw5O}D%(UFHA>dIVS z_`zamii^MtZ>Dq?oZ0(R9Z>Y!+@6CjcuxPUA7rn4TfRSCtuNtrif=uCVvC=gkAowk zt-mX3`MBH0b)%A)$P@Ib1|S%;>^E%F@7`(Lm1#WjvDKMbn|YzwD#U2^;C z#rGV%x@y&TImBdCDnf7I#rIVuv9NGiI##|j|10tg@!~-+JBPOi=sXX#M))@s#xi)L zRGQ>WdR;$t?3re^KN(W^7~;#jxV|}V(!&B(CI$T&hYitU!RFAzwI*MZmm7IRr;LsO z$9B5ah7bXEMYtk7EEnk!oHqVDESd)bKi{^lIJQH0ambg;(p{-<@M%~ zS2~XnD;L4_LiMk_M}0iz0> zQ~&%S%P)|QvhoEBW=aZy=M)8irLq~dS5(%dPKHojLzq=8e$3o;&IF1tlpBe6h zw=Mp+3UEl-6VPfmAy}h1Xgp7WkDkCTVW|-VN_}Cj8!O)I(Mz{fzO82Dg53;NnI*?l za_4;-?%T7xj&N;3x1v#r>czm83l@d{AKYI0L$AF_8g@@xk-{4lg@$p4pCgvpBILzXiIpiq36(UJpQwO zCQJE2eH(XF{Hc;Y_N$QJ9`DjXFM1!BXj`}}w>Ug1S?gi)=>9}Mxu%h3+R-#Sen0tf z)Un|qd{O*fW0aY+tI4>Lc4CI+`)c5_V=um zY{x@P_<{8SI?HK`5uqceu{7Syi6%WRW5MF(8_&;;(M{8Q| zdW?l0&$!gNwQ5`LCOzj^Kw0*fJFG-qjd~(ilnxp%6*jssUsz*s&uXYt84GI9RDh0# z0$Fs=Fl||vhr*T#>~Rlyw6BS{>nV))$s&kpGZWCfrB&nh=y@mf4a;7^KA~Edu}*88 z6jpoDF7mYY|4NkCx)~#FJ%=*5@FI5__H%TC5F67apLr76Iyv`@MosyQ92H)F>>t#p zP7A=eORRGb*(Tkg9DM|dnq)^iQ;&q<;SuRv^O-4y=LHjmr!#U1kA~W^PssB5zd^u^ zr-jni$-0`M^YTxznXFtsLEdlH5M{>ycw-4^gHuqnK2Se;J$blDvQSALKxf@Ad3hcC z547ECM&%I`wBZ6!2zDVBg+tAQ%mk*rB}6*I0?`nJza}64gQV7OQg-3KhlZ#0jt2yy ze-GENKdMW%pRYhf*;TmUL}_Jt1@YpR(qT#4^9(APy~y$55>5#>x=}D%kn!(f8`{9IT zy1kxQkD2Ke5ATxo@A285?s#u!#(hQ=JDw1b^tPzbO;3F5%@r3x#e5f(wWYZ9Ae+^E zQuy(dQP6(_#`j1kiYz7NCY)(}cMq@R18rNjI?j&XZ={7cUQ{zTUMx_aW@J4Etv;-e zFF9NzSjr=oLg&zTE7~=&8G_6|OKlmkBaP>;FfY8GB_Uf!1~h-Q;IWcqMQ>OP6y>Zc?Os5L=>yN5iQ~(WvREUyMU$L136fK@8Ym9A_C)8Yh^{m1&Nj4rBr_F|vzZfTC+Vstp& z9Ec>RA4kK7J%s&Xe`*|Wfvp$6y@gIsX@&0B9)xbzAVL=d@j|dO8a>^{%=hWP%%gfM zP4VMJ4c=|`j~lMsJXhG==`{pijbgj}M7*VKr#Nsz!04mt<3q4UX)$<+9_xHA56kh% zAM0#D0?Y0RLE%2#96!4Vx2LCwRUwu4+iJfc6-wHhV&w9?dKFd^XD`(vs6l>e zu1z2*-jnx+Cdki%XbuCZXt~`MT1gvdIM+EFoU7uVNv6XNcD3fd%^g+n$Wqmx2*|jn8(o-_W$4E{aI^8QsX;RJ9WT&{Rcd zX15l#;j2-Se7Gh0qf1VZ1A-w5Jr$jtLqtVJADGiDPPgJRl9l!Jr~{Fu60VpHcN>`$ zQXEmxl{x~3J6w=f;VX4ZH=;!xRkl2j1}{av3AjPNx8Z;HR63Q0Km}qDrvyOw1o^1C^(pGqb z=-K4VH=g=nDuQ0l8xyGRIrHO1#gpB*ZiC&k6(ARdd^ zP;d~okMRFeIUpjPk7PM&E%Y#CdGBq{Z0+YFP(}uRP+DK2d<+#`vL}4H9nZMmq-;3t zPq*DNaZy4GpJmVIbJG4@F)w6Jff!I4DY+9*c^rX^j=dg4dv>Rc>m+TV6r17U{6g` z%^+Fx_f2KNaNv%zfq0Z3dlMG1Kh$or$;^IcX+47ZjQ;c6mumy$AI2)>66UP)>#Pgx zA1sAnzY)vwUcUAI&a(CHu0-F>^XM`ek)DT;Si}~&8>=xdE>77ueN5pJN}oJrUw>1j z@aibn>ryq--3MP=o5soHiqW|T4eSHe0(;JAfveWsK;tPRwM@7v95#56JFINq3a>B+ zHCg5J*hAO#>eS_~-~Gr0S-iswHB*qbO=Vi8{F1a$c45c{J*j)gJl2KrO-~cp>yaPq z4owBScPkVsXAN!1^>g}caSWjlOm*0bxXWPV$|zXE+-w`T{iM1!1{xxb?w!){R5va=MK-n zNIwqHm8L@&w9241t1DIblwBEIIW*psH#p)GzdB9u6(A}@0@~`OhQu=3^ zCI4+u0d&7c;B!4(0gHFUt$@#=(#NWD`#Jhvld}o5MR0Os@0#b;H%Fyq;2*BkrH5tU z>uVmY_+VWAd0*=s^s_lpbDhoEO>Ql}Dm2iOAh-?|xsj#aTWrM@P~LDm8ZX7EX65{!g$dX4d$1P5A)f2y~Rr z`_w*y_)GVxUMMGgu4~?|`TT8)-HV#Bwt#T7x-$Q!R_N#QK`)A_Xm@ zslEb58Bbr>+Ae6?9mW9As6pCc(Mz-}N$SRRb;P}44Zzbc?)NNJhV z(d(UzG~6f*^d9Hl4~|4w$-mFQOUI}%eZ#BxRZ!qnFhn?6=*7zr)J5}X@wb>kkf_9r zsPRpK{2kN$GC#$W(!%zt$dEkGNF)kt$TQD3fq`wQd_94)2c3W!oMPQU$8eYGdXWXk z$nnnS`LS__H1xgunHQyH;hHkvG1q8io^JCW#sQ+4U_S-2B)JMJ-YPc$WJ1A=LslN1 z{5$gB^1+!&wm)K^$tuwai|zo2`yf0SZq6xBr%qNn>t^c zh<*0G&+M1#$B$b+KPFc~9M$3@F9ZpA(#5E-vy17aw`k!|P0a}g8o?~% zy|km&rcntNn>D_rO;Se2&4EGMRU5`-j*y<*>7(-8wz3CYQ_SG-H%JiB_vP0qK#Xt+}RV;MW}y+ttT)aT;u@G%V6e!|Fj-Cy7` zC>q@7!w@rr!vgV#vXPFKFAAXmi~^0YI>=9$7uWXVYkOQPOw{+J4ORP1uI3z~`98z^iLl=WAA@I`a$z5J4$YW-$zE2btBEdn11 zW|z8ENhb8+jJ$NeX&0470Hu6rqC2QLjk(jOhSd(BK%)wSaIx}+j<;%2sogtCl$S2V=F5ufEq~I3Ke<&$V z@o}6_%Jdf!UVj6FyOUpvtxMgMdK4J^1SBA?rX4Dsc`^Ag6w3)KQmeeXvfBSF0 zj3djxzMS}j=p?`17-!BIpLQf(9Tv-M*rD(%{E|?iKOR54A1Wp8_BuF@-w5*@v8f%1 z*3Qm&145j5)E@Uq4{faTy6iRqFFAyj#M!Xq*wBG9N|w$Tf*Qy9qubpIm+Q-m7|fG^ zKl1J%m=b~$+PmYtNy43MC=tw6b0Sl~&Y9LwZZfrKTUC#g_JcUz^ul<^9d|Ezov~?W z)OKETNyyNNU|J9(keT#Yw zjjR)#%g_#DB}Ws-p-Fx{oz0HI>DILZFi z9%*OE${Jb5?JD_7`wdMFM^2Q>3v!Ilw)(?&iMFug#R}M#JbpsMEs;na<(`sfw2kd@ zbaK-s`MY=ZFqcUG#`(ZyLq==`gaJh5;!z5DS?%Z2?CPAvAvcOa3OnMufOpAu{bdz3 zM$76XKgbV%{VK>|mE|_VK}UXt?C(_9Fgf3tVSPYwKE^x_Targ)*tUccXrokZsPVxP z_C_e<n7OS*ep^su06I$(Kt6U`%}vT>=Y?F)cZ^I&B1GquGreb-$YZy}4;6vn;McwiGc3U5uQx zZvwg$FK%&(y(^)*c|yxJJ- zXV`tZ-672bWx9jR2jaD^$g;wvUD$|p^>p>oLh{Sj2R(A02GSP%POC+^+h%+g&ud)* zww0kY2lh5lRH2n5dv!;G!7D%!x~)ChRZvC%Pq~R&a7MOizgU35ri+V>X4hFkxMr;z zb$D`_;h(#VL!09s{-g8}(GzAbTaWML%m~A1c?`pf#>3E7R-p-`Yy6DWo8L)pR26&u9L-SdlA9hN~Y=W=OGV zbwaQK2cFql9Q&q9DFL+UqinASgd6aI2xfQ^3uq$Dxs(}^t4aSs!wj{r6PL(&X!drS zelvA)Hn{(GaGdn-btzU^9w7xIlR)S)Az6~rHatqM0`tXeaaOu7{f}ySW>qx4NI5IM z|MGs68%2ljbugFLq>hrvIi8ZJ=WAM8W(%DM6f8E|qB3BJU|vYB1tC{XwEqu;IoqZD zT&oBDb=wy)4XTjX<*e!Tb1e?O(AIpUK1|BNN`8B@{57XLJ7SgHZ;ZcJ6f7Nu>;C-t ztUfdGd;NQNsDhvc87>Mmrka=NHLrN@qR@RGgSu_W$;#SycL1?KL{kB3I`hbjTy7i9 zo`qyR6AOuyzA?O|_I8D)UA{XrhS*8RL}5TWc?)kAURE&p0&jA%b#9a>!@8`Nclwit zTJP~b$uCP%i|G6fW-_t)B}6aefJQ0v>rhI6PLfS&KJKe+1(V%VLog3l+fMn_ zX7|xJO@}g%h$~89;4L1MVZEw5V1d-4v?RB%IcR_X-7m|7pT7>WrxX~94X`1MqXt8z z{46PYeT`yTbo17zKZ#y|nHpOMP3Ag}0dLS6CT3Qo9lcqd6I+VQ%L+um;(fm@6w;i9 zj2)=w4GnwAdmub<4{So^d{rMvSrL&b8edd(r@XVi9gvzIKI8IK<1-?|EgCVJ`2plN zocXLok`$(#^H0-920{otwurWRPYHo;8+6jw%GAs=AGN20wm)_!r^&e>Od9#U-;!}8 zaXOAeb1U=)q|71=V+fZnOXk{*QROX#<^ZID55Zh^3wsgF4lxINO_bTzxQT+y?hQge zRyV1Np3FU>(*~EG%bECwg;4`g-8`nuKIr#h$TFzT%5r?`zsQeE&5-i&Us zM>2R)*4=${;C)2j{QEFk+j0UkMq`rG6+!bl(paQD(;}3*oYU%6q01)~McqjsdnUqj zS0qwgpnM;vu=F@#K# z^p6f-K}^@4?(gRK-f&kHHOij!^zb&5*SOM{hAT1CnS%RW&&!2AN-GEGDv@x_i2if|05hEx&YG&<=k3~_1 zzn24n)6!}KfsFM2wGjXZ2ggAQ?o`+mh|~AX;Eus>5!#=rIusI14>-;U8xO&0XsT?A zKcz29|cKf_W`p8NU`rE!X=G-R}o+Mn;DEHFpyt zvl@;pD(w3&vzkNEQ9rzY6QN?e_Yrk>cc+|d3>ios1-N3H+ZNOzVkAL0ye+83^#DPh zU{Y7r*TqaeaZ8@f_{iydhvqr;%X9?cn-@1QQ~bE&x^@0f^*9y_G^1h+&YRg|$@yEgn5iLnlWiOC`S!-@{&h(VgrWAIBAn1ngD7*Pj;q zxvbc|HVuaIW7PEg+qB~E&?`fn>L=%af3O75*b^#g;G|0J-RNS1g*Eu8(3z*DJv|w% zGYn~9$Jgk?b85ZY7wd8#8Tj9a(lHYCP%VggTa%LOQY*A%G&1_vPUD@aVp>N((^N6) zQd$rc*n2G*(zB7W9L<4(LhzSuYg3VxzeN|qgu^2kC+3fBvsb5If8No7Ngjn_gJ*pK!r9Z{$Ax zH|Z7UeML&2cool@R^ZmF}G63eq+Z&ldLBwIuqIlb|CF{VHR$wSrscLCf) zU?#D7{-%Ca;jH7!{lXo0zCzj-h#pC&^+^+zL2soUZas##We=hIV_O?P^qYD^1`bxC?we+hNxS_KR@6)FH`E9m_a5?TqAwt)&ajgL(W^^x?#_ zV7@Tq2O(iXf`3&u1?J5|2_h7$k1K8dqLWp~an@U?CeQzD-(%rxx_NZaD`|;zjCD)8M;o+$glbp zH@dVgpGkcUnQa?p zA+N7RBMV4nhhyuw?%>L)&N6wpt917qaHnKILZa&>{FHYVYR*U*SVNTXH! zZqa}n!hDs3jBk06Zu>IN)!r6mr^SKd$IjDH1=u|% zEN2dWUA+;jZ$JM~W@Y<27J53hqS@!(aW62t)c8HN^SS!Ju<2M~#JTD9=1QHHOilt! z7J9vhtrcncs{PW8?fzU)sEtKZ?D6f`?WQe1yoXwW&%icE=|`fx`=PJz0IjVxNTr6d zWwg7q^9JDCK^%=Rf{p9f8w@qm@5Evw5|6^Q$}-J6d!EHj6lUv@@JIR4ujEOsw{Stp zYs{&;3r)xF9AD?MObdXFs8#+I0I}ZFi3(fc24sHEM`GQwFjZK7iz)VkUL3Ke@E2&R zjT!Z=(MY>t05(6c66;rlg58hY!5n7X{IRd!fEw0-EYFLS?k#My(IF=FfEL`Khslx*lF*DFAPh%^g3 zF`7T|?LKE-bTd0UX@qS(jQM#Z~)gLh(6o-0h7rP-&$)_z4(-sFITBx2oUGK(oL|3tY8Ket@E-ggn& zBUZS%$^d&_DU2OBI*|?2E;C{UT~%M*pU*d=|Dy!Zr71gG)6t+-9#ar3KcSa5d!vUF zh;*lE*zELoXm?NkI!(rV{hX*LZPl!o(x+Q@oPE=;w`9%({^fpsMdRi=EpwRS_K=Md zwp=pjJp1QyRbsE}!jh(q+Sm&q;E;0P!zF~Z zn0Ey>>v&fy2Ig!;FXgbT|6JwcsWIO2E;!?PkoZ44l~`p&eL1kxd(?YeYg~`avFNI% z|A=WT`tS@(r0xfAy$kkH(>)=Z((IU2sn4Ach1)*s%7QAS95cLJ9T#E)@x*hcUnCsi z1E(C-)s}f@&Sv_?F=h$LdP2YG=~Rb*yymzY$cL>fpcT?0T_nMiSPRi2+A z69qn%^FPZi3(MQ{c7FV)c920v)UsL0&l!RD0mHXf5i1eLiGhqO0mj!7LU#SMOJ{Rm z>w`7|6fIY`4D`v+hXJ_J2{o{JLAU#v7B>UbX~{N|ZL9)=yN0IS0DCb1)-H2dr2;Nw z82|$x%&27mt>;*QQ$FH&*f6?9IaAF3AG?bhD z$?_S>h;Hkp6*TIhC6}BmQggV+U-H^yfdU3QXC3SEz7wMvj8jTy0eyR;ec;N+?VCI{ zP1{Ki3!*KHWeC_z)Mov772LZG^o+H?P#XA;-N#wrvgbPY6tvp^pwDzn6!O`+L^4FoxeKJPrjLUfQB>ZO3PLnM7a-3jd% ziffuol%5X?YY2ANS-XXrJ~akpiOzQ3wVo5Z?w#Ch?%e{0Zyh!=2ef%%0}rmSWiy&- zKueT7n^15I8`DwKT=Gbd)ym+QZsO-@?uqF)G~d3a5FiYL5P`eI{3h4?y`38`xJ0!n z1*ybq%YQ7tfB$)=?)L%(6%jCWO8B%n5Gj~3G{2VYEFAtRC0#6P_A0067#B;#x*q4- zLhoen;!n$Cs(pF-Ou-+V59mZ+*N&RFpH-DJr17+l-R)B)9b7MgsZMvfA8 zWm@@oK2~P6_(jH9jj&7e8crER4q5L_Pms6CG75ul$N6T%on-~~=Gy$jmYqMpubrTX zznOTrYdmzUcP^1Rq`|US59Qq2ePSMAAMdc zm&0~MK3@3Sx9@UmHD05NeEmCN9ZC`H*R#M^Q_buAD|b^*XXEsBr9aWu>>6L;>kj-t zU?0J&L^7fkNYO{e-8U%Ss(oo~ME#G=NsomP{gmi$g zLLjaQ(Ke`=?@Cra&YnmZ1^4owgSc!yVDgJ#*}yY(Hexifqp>kvC$zN3%6lu$`Zfw$ zo4x@N;S%P6J~Zv+okokB(?Y_D5?|s?=!n@>1lkC(iewz_RTkL^E~>=DC3m@3f@OI# zf_!?E53ER7+sZmyEPU|8)T9GXTf+FZH9DXh%uwJgE z^-?W!ptaQV%+y%b>}#=ft**(5|G*Me$4JEGm72eu^xOA0QIy~V8Ayq8bju38<%ssm zIMqNJE!e--Mt`)>HFKwGcExag;pp>pU?{P{S9>c=-g2t?T3Nz2?F+f=jt3(sr-IwN zq=ewJPyJ8u^9-SO6Wo&%^Xw*|EH?q8O`0YFku(4}{XybZb%w<&L&N2!D(;+k8ICPr z{#9kpfiZgNirwcC9_((v2XEETQ?zzOC0~Cv;RbeFqN8Z2+~Ca3P^|r9TQnqy?Ye?p zcvWu<|9NedhqS-4<|q2#r)}Gm986%d_Hg!<9AFw)lEYmHELkiXg$od0t$JW6>Gy8=bGOnn}PazNfIb~ub zal!Ew2D{I?Uy{sJ-TQgEZA_TC8BPeOpV$aOk;qosXDV&cl?Z^X1RBtq0w7h$AvZgY=52T~=KMv6ims*xNM3 zB1^E^P_c?iOCxfS?tmr+oKo|8LK6&W+S(RYKlAF^)53x<1z54JB53YRn(YKJ&vejd ze^)&yX0|>(r*QM2wk=!eJ-&N3u|?y68ewucKn)-l3h;Y?b5`n8-&#q^zDr6ZhZ}l^crwSC_B_M0D=X0v$F^gaf+8MfVad z=l_5vrdNKH8Kajpw7lX25_5A!n*?L*P>Wz)o242W`8mu8yO1g``A}Y;1O)wzzW|GE zDK&;w=yE297i~M#iUOZV0nQo#CZdHbWX+x*+H+^n&HD$LhBF6u!7m8TPxbl0zP|MQ z);!o_hE?0yKvSE%FFE$(HxOp=t^ZqM{tW)2%sw<=A&2Fe8^TAS_xA>#L#shu ze~4840J+c)m2(h~V;hJf0ix|%P;9+f`uc8PJA}au=iq&!R^A;2H_?7Mkw*&sV$0mj zM(?()(djN8&HDql8z|hp1JW(x#_iuXwz)N>WYpD~sz5H77P=WU*};LP-x@jgQ*7E{ zvsTcquGCD%`}-GiDKcK6=vF-5D=e}`;b@ghK`Rd!kB{3TiTdXNn3+jjnwCNbnqvyr z6W&;wXre_t1DDUp+rH>`1$^uoctsdS1akgnoVBG9sovy>_C|GcOZ1Pacr}=@Il=8+` z$aBYANDaTJoK@0cwJJgPLB_|fcV36f_k}SE;Z%dG2y&nl-o^2Fwu)M+KtaSNYG1LY zu9U7dDQ9$e8Y>&u)D#n`e!$5GHI2R(pX@2IL{9O1z1)DTZ$g~Y=uELpH=H|m?n>A< z4vLgb7k)kh_O)dV4WA<;OI;lEM&xW30v~#CQ$DTb)^Z@hv*j)ngQg#T#bbJMOhdbm zA|}<_QX_|XD+>fd#gcu4!`mNN+r1a?7^2B!J64eK@Cjn{=YmC$hKTqrFwq$bVlC?{ z?Jel3gZ<4lfLBS&d_m;OU&TnIDBm_zeOKb?xMCT9Jg`sV?lV8Si2K5khU1-0CcB`eAE z7#3vld=y=JT;tMPUJ`5F2}3n~GY4a1rno1(7U~F@RYndKijfFcd>a14OP;Sbd_TNTMRd?qL$v+@xrEZ)&s9%*IOvWD z_9>vW+9E<|=Np@lJ<2&_B3CwFbn6A3YMhsww}_XI=%B^C34X#SArls$lBoZ@V-Y9A zga04#;4DAG6e8)EUy<9~wfwt|6PIAGF$=IjlPFTy`GA}+<#S5k zit|m2ALSxsK8B>)x!4DfBL8@#QHJ{aG7hml8;oW{$Rq8$%sM;;0XczM`CsdLT!p** zT}8%($IDH1wNt*|PG}e<5z|bzhhngccE&%o2lqDw&f&{b0~6=y}(VwKx}7~6Pr zLBg3-4Ul!bx<6Vs)m#QP1x5n6f)zGvZS{HwH_`Xt>-XFKRu8*CA2tm=&RD-dxkii- zQicPe_eU z5xM0)*iZj?VX`L62HK}jw%?!c=PYm8-B}|;z6r{JAUk>h-LtdY-bPf|a3 z@*O#d{fm{7Js=7mGIcgp)G<(B^iy(4^hy6nDW5K9`50op3>FSw#QjMHqrf(S*g;5t;tw6Xw|*-RAmay>z{ys<--C(Yr_LtuabJD_{ID zJGd)CEY-xl85JW*^7jX`DNOaUlk8&706}2z78)bhw}EG2nD5EC57#I^*!YM@6$TE2 z7=KSIOeI-!%XCxnVJ*HJns4>uyjS{Kpv;B_$?Zk)zYKlXaz>%sgy@ghrwXh08KoCHA)tVjXE31>t3ts8^@)xp;QbP25+VSK}l-s}0@96i?tH~3Cv!oClo82fz_ucaT zrl;Gw)>Rya#_R6M*Kcf=2ZA&xHU<|@)D%X?d(L^Dr!K(Mw5YgH zFt;vsf8Vqyr8$TIWzKw#m$$;S$Rtz@h71S`7w9-)9LyYd1o^R;&)>vKC4tCO1semC zJ`>x}%izci4Rm<6;`4IHHozqjxI|+7zAbM6N*iM3{NNE4X}5~xBC()Dv;g9@T@lZ6 zih%fAYd+$+#ri*OSJb4ghS*Gu<(YL^@pYds+Yg8-inG!Z_S4O;=1l4rB^-o=Z2k6^ zIZ&NTuqHkY6_y$2b4%KIG9%;jvTX7fDyKT4&(| z9?2ONuYF66v_Cor z=tQTcii^2&;@Y6+Bt(?q7MYUQl`H*zn#-)MPJqUUFA_5uVKN`&_*3ER6bn9$p=8(T zgcvMkimwKO)Wt{c=%QtIrK3LNw3WBKbqt218awXo`_+rb3{B}! zNVv%_B&23Riod>z3fW^H2R?y6r*4vancTmHR=!m-8DXU9h%aA~ykLQ*av zz0l4KvJl0EljyKKkeve zcnxr!&ykQtu!Zk#yZ+X6`zsHJTASWrzxPku95uS;f!dvg386C#QweBF8bjFlT_1gy zuFJhH*1Sm9_DV?(!wO8O{;FGO?fkuhW@JFFL&`5g}vY~Hp<$Kz!tJC-Q##8sc)(Td&fP z7c!W@oF(A<8P*btf?Fpr9&6B7>82n7s9ru55o6vEiM$Z6$wDHSEBD)Ig?<*oeQsiD zj(RWh*Mn^->)Jayss`w|ulXKzdUDEE-=(@uF3kccx^-*(_Ei9Zw1UdXND(;L6;?B@ zQXMwy<;DhUxfNvMxI1lXy&C&wPyyK_EsUCL-d6~J zu0z`EhSur+QYhi}a4LUT_ht&M;ph>UnPSAZ6QOHQfti4SDCpNR=PfWd~f#6|zLF#v{GO}^ir zR?S#ViCF#FFUj@jbHS9_P0xgi>44c4;{T%UT*I>U#KqDx3#rQ@F5}+9&}&oae$2T4 z$R$J-x91u1GY0k})v*OU(hUUd>iRZEydMc8dNWSqV))Qew?R+;E?0t3!9$!?8fVaE zNSd;naAs>)IpS9!sb}1Yz@%tJap<8C&K~ICjUkNX!+GxuuZe83$!9}UF`80_tK`E( zF%e@AE&@&eByXvBUE6ox@#G)QmL6^t^V|Ud-VyYT7qt^}6JN~!WYc+Y;^3uq&oY5K zx1J{JQ%L}*q-*$xPy7De;4qcy(9;b(6$QNfN~lkuh&{teX{o{bolQm87Std*Hg(3X zu)CYJQYNoTk2W`dzx-vep}W zC#LEObRvLY-wpYlINm`wH8(iQfWQyQ;!9ULkYS%ZUw3Shxbl7Yd{PncR1VZOMd2Er zG#od{2pv4R_e-@xyj*SFL6;*<6^||rP6z=!D)VtyLq^Kr3(?;DsCX?2M@?X({2W~J?%mg~KO}U7I9*gE8;ITLMA19^yVpC8>Y5B#h<9(wd-4gmb9t7^loNY4#{*i`vqTxOP6+KAL$p_80^Bk*y{(7SuwZZ9K7f`r?j zz{u0T+zaOBen6CHvj-5lG8?et1j4eNgh(@#Y12;xIySKchCIqGaz5%-7AO%s;Y>s= z7-}u}13@kz5?959i#Be(C0=6@i~|t}O zR7au_gHnq7ZEk60eAzYb!m~#Fk9P1*hFR;Cmd;BAtLq~2s=+c^r1kP6|7gN3%ClY^ zo3VRxCrvhQoB+>^pE5R?(eIq@y#u`-e!hRILF4^!aIkEViJ0`4+AQVZ0of@%_3?+&!K2Uk7(O&ld| zkr!Dvzv)K5j-D~)U1|0O-EaOR_4MGYe2bUv?HQKhMl<{HkSa4)8(ZyEit)JRA4s+n zqsEDCYDKQMw1?hy8F_z}s~%wJML?VPcV#EGIOJM!-juyFoQ2~HbZ55$ogSOEjS^B) z`;8$%?z$)e%f5UJsl&tWA>t$2hg*`v#L2P&lM*doUvyaOHcUv2S;y?`=Qg?v@D&pl zmYC&MR%SF65tH^>t=+v@)k9s3THQQyWrF= zC58S+eemGBvD0J5*dk6YM6P)N6Sp^cp2}O&-S?^C$Yxw;8+K-ezpcnq(j-e}e;9`E z5B0PGMU#-a*>BBoEn8a?~6ZGSf{z45!B>`mWa z7={slH4`0k5?%GXwP~Ij*uT`j~3FQwPP@79WkwQqw=HQMMhTe}`oBbf6Qd zj0%|X9271$rskC=;Dm_3Fv86mi_>pJO$o6d%1?gXQL@D#CG`+dWXNG(HTfOIuJfkNxlSwr?=2d@k zmZ|DY`$$s?r=tVG+~FD#yI;$O;N`PxeXiwAe2)oBQxk(QqDg?ep>@szC80Ihx+Qv_ zH*^hQH&+lJm9m(K-Ns$WNR*YYs;b!5m!cv2gCrg6U);VS@S;+PqoOg(3>zm|XNHd! zl^!lEcgzI5z}M#6`V=V-2@|ipV>`t|&#Q4lc}_s;Zb|F7IYjI}pKUh#O7Y==-xFp_ zS$<+2b7<2!OlG&!2)%$!PPmnwCM%Pm6}m$O%kD>|OUi!JX2gYVCk;?@X>}^K^Ja8{ zyFX6_*3+p7Fdd(+ISIXIO|hZP7{an@4SOjlaojk8YCncCQ+LdQyLztSd8LY1?C2De zk}NXbTW`~=!q%v}<>}k4hTF5L?1<{_b3t9Vh1GJid!!Dt=5x5Zy3DYCqtFIpo+W|i zny-ow<=BoR@n6MSoq9I0TxO?hJ#yS4uGCElYm`sdPpEbt{9{B$cyy;VKa zZj%!Vh~lUk(c-+~q(6kDptdMsG|KxMvtOG^pZGn)-QL`n>0AGn3hLmb5fROOZ_I|j zuNJWxkVNSpAF$SSx;v$XV<|{?om{N(Fm{wNb-&lg?oK3#y;pey>txHb{V$n=jR)xOlRmi|;Uu1ZQ{$~98Sb{jkH zleBMfG>fOl^)3YvF|oSPbTubHH0_Jo6hI*;w!q8#yVhWXrTca0o7#>L(IC<5G^9>P zURA>1<)ykxy>gswcW*;+8Mcydv$l5h*-F-4@Xh~I+4)Oo_GRn$4zioz!>a{J$Jw*n za}?nQ=8`8_=T0urZ6}w9)+6)KFU)GGyXL?3oO&%yKh0L_z=>MUZ$$3z!1cDUR{67~ zB2~^?2wRc)i)xgD>m}?boPn`%ojH=nCEW`(X|hkrSk&ICK^-|;&e*N!Rfa-3E z@KaGe<9h2X>Bg#8V>oSMLUq}4gh7h3-k8B@rA(tlCPj4g{0 zODP_jM<6~aGHUh375w*a4k`Z3OS*}0OfSwywD5CC zrOE!|kYb5`?*fDQ!Z&FRGg<~AJjL_(`I^quP)+-17lB66N6dmt@uMNa#*}m`%7dX+ zWjP<;=I79T&dkf`3t=$#^mU_eG7OLbIL#(TZAMhzGReRYI97 z7{+C(CoW2jK|)Rup&I~pXJix8p9F3H5hVKSm-@$L0W4O3Wk+-fQ*n@v|7%RX3UsbQ z6V0Uv{IfE#6ekO#yKCDz^Tolsqiyp=6;T5{yrLy2NdtNsEDqDms~;`!>oX|~0eE)LGud7w5E`683->A%Cu zw>rb`lLzuPdpRGjeXZK-Gqp!t9T{B0xavgbrnfpa2ejvc(eS^g$c}rva`qm){80`~ zrd6tHZHmrh3jaC|ltCHS8)A^!X^%jCe-gyFeNFjd#fa<2k(CW)VQu!N;BhgO$ygBx)|cMbUF8iZ(i7L9+K<}|W| z(f0aI2EDcbvbavbHvrp|Itz2U)xPWH@Tup-4HtLcf2em=*h~^QO&F*PT7B3IRaeF* z-ep+qc&5de+~{&C8}T{_UePR&OS5Q`^3+F`C;}jBAF!do2j@Un~dYRX2u;L0B4q-c~>vXX3%kdY!I; z?ZW~{%!My*_8pzA$;i>RX8KL6Ls%@1pQqW)zfFl8vW|`!7@#bi!>6oo-cUnQ1><>X>?10aWmwAX*;``mW6dbU~HGs zdEU&b_fOZW(0xayfq}*_?!7Mo35F!x_^d=Prz|&mp{qttB6O8g^cx=eY(_FKV+A?! z_w~(UQ4TCPUR@E#E| zww}L2K&z6&7%pR7qdM|aNz_~S7QJ35h`}hj3HrhUc3-4^ks)#{Ac};9FK%~%Q^YkN zqr;S~35_&dNtD#x$?@cd^O$=pPgrJG;RPEdC zp3GS{JlsrF4Gc46R=iKY^p%NyXHuH&awo+(lgf^o=FC5ENy-t@wWQCA`6EKF4#%?8 zaHL3sy~g1Uj0R_nu%}{!_!dgsz_;X8P+YzZF9ono{%OvWV~uOYcFCL1U%WOc(Ve}P z`jm=f@3)E1i3I4Lu6M=82|0e#dUe4zu;BG}H}YMeHcoB)&neEjKU3H=_`y1=kLk;! zhEAJOQnU=9#QUXZw%>`}o4QCsos~!&IGPI!O>zn;fiNBcn+@<@w_iKYkBtFTJpm#} z6-g?xEt|iEk7&KbzgcR@52&vK~7)bMYjJn18kLW>2?<-!lzl-sau?SFc0*f>g9s1QP#g zgXgLHiyw*h)14#Xj%AgU9HU~Riya=ej}c&0N5K~x%ljJtAyL$N#g~jsL3vJpT(I3EcfZfh7OVPus2Ae=$?KmH5#Te#-RM ztLgZCZI8#x3I9x4;Fo*jU)U5aZJ;u}7&{>~Zs^h7!^+j)S@rkW>OX6IF5=n#h@>$f zGf71l{wDZKkXTO7rVahjf|9^L6Xx$wT4ex8YQYQsP*47!Zw7#yV*f9=sl@+}oBBTo z5i|HV%df41n_%#i_|j;Sy^11-!1vfubj7PDiQJBqB$vGXom;@674P>ta6D@}eF9J;;r% zy8mj52_~jV_qsJ{_?V*>k`$!R;w?Bj{L_2(rP#zvbt~9pRmHVTjzi4xSSi;KdM}>jMk#Qhs3%vQvP$>;yJh8 zIPZXlg*|}d$q1)A%>K>q)sAyK@HP;ttc}RZ)_mFMA>j;*L=QlJj~S32Rn6r~o6%ng z1rPaDnZNp;-f$$4_zylYI%mKLBY^#;Ca53V3a|8lbZw{n0jSu(ob9imB&=rvQ*xT-TkKy@n>AGUN+er40|CeMh`umiAuuy>` zUfNf=ty9vsz$`ssr=MmVXriZ@<0>g#W7lmqYGj(UvfKbt!sH%8uI~y8L;1J(JwATE zIzG8yx#8x98J@UC>D=L)ZhY2cEnENez0{gWqnFtnq>= zvSKP#=z|wMC5_x>i~?kmcG?#brgN>1)v@!0P0V<^v<1GSwp7P1JiJahkCCDLD?fm7 zi4u?bHs<7PG8Tn}eKv4*-0ol7+UDMsbVdR5G>EuRSLnC$a9`gm+}`gmp9eCZ9EQSo zMzpr$_BNB5^TWPvim}~(wvZvn0r=yGJJfIW?X4J@H_Qij()zF>?EcWs3O(G4mNajR zdEO1HYL@Od+Wl)Ih_8zDQZ=TiBMjl}jj_S+!VTA=bchqdLSiv^;8|bDL9biK zPUpc@%%R=7jWRw!wG}UJIq$xsaf`v2qBy*}9g?+wP+-^%3)^j1Xsf1;@@S31zD6b?;U?fjuseT zMX*hwqQT+|CPl@0`BgDRDXO1RpV|9u2qebo{W9na zcHkJF_$vF;fq^4j12~<8>Ndir_u4WquQK89<{TKqf(B` z)IWa$eXN}06WA2C6;M9|Vg4_frO^QZJ4Eq;i76I#m=hKj1%x%!`FHy04k7R@g+*35 zjcii)+RzXQtbgqN!!XS_^Bvn81NIkFfHg^!N|@QTlr2ZtaR-vgo!sJc%otNXGY8M zk1}38PBtU0;lct$w42h=PSOE3{;K3%KyxF!7|*&dj3CW!IbPrL9fa4|L^Nk?t2csbKm1h9jGTH*-UjO-?YX>CWXGS*Lqx=}SX zw|?usIiBlM13i8M37Vl?$nnR{S9=ax9cv9LuKikFPZBa#8Kir>5#&m9d`1ht=df%s z!nR`nWt*j^kv2nJf3Y<-wmqdFx6@OM7$_&37dy%*l=&hUfdaGueUt*wHuYcX{XD#h1;kI;0gRbH3MyBp&oC#jrA5XHD?qv3I?h%9-{r4Z}Q#G zi#+@DfOXolc%U-BMs3ieZkXXp4$bgnrNv^dfGw?!Q%!t1sym81p)bg83Ye*Tq46#D z_q4$CRBwNSYTxC+#dUzQAbdER;`}z@PXTkHu+3%ZA`h?H29?wgPx3JNF>*`aUlMbmzRk}Fr;_yOuX1wizsbrbdfihnaC7-Cx{{yHM-%9u>Xf#!XKDb0s)-50_tZJ3-@DGQhsZ(7zwR zJDWav#G<|Qc9$QE-%`F%K?N%UnzQ(Tbe4R#d3pBLp4I6^^XhhjY^%)(g2k)Rb^%z^ zg#Rf}H$R+(A0L?=uteG~77lv_=3rfW6WQ8)eQiNu_sy!5GPX@xh- zl3QDNf6V5XQ6dPW=K;NoBZV*D%71*nFGE5B=xh_`?{6Y+7Fwt2mkFpQ5Atd)k!d|H zzLz~-G=@DFq*1$#qnIjj_bF(HftFfp!>3Cf9Id4(2xtlHT&v$QhhXm+=jG(Y&jbJo)KcxE5CeLx3&EjJ`@cH3@F7ZUq$)u~1_MFiRG!?pwBYQk|MW|=DeTy2ya_RNzFs2+2pM4BwCvA13; z?X2E2($#TpUNkj{gdKZfdOuh8^AVJSfCrVV9#M@j&L>WkQ{$^lsJ-hE9X_cmTcr-+ z;B{_*6qi4=e8q(b8a|+Xsv{t0BV20cf(R&Xcua@6lv0A7wVy#B?`_NA*XjXJ1i)`J zewwMkq0fQF2O7<#c;O;Cu^cnOSb6?PwZ{|W+eVUC*^#1Ws=wXRQrjKKV1n0z=_;PW zN3~mFAm4#ix3I@#c-_VaO+v2o0U8||vkFGX6<}?`*0NL1p&J{wc zZlJ*E_SjBbon340bvCwi`#{&Qb#K;i46Q$%El4~aty5fRK5QV-jByuq7bMfPf_Xhi zlA2$CNN4zYsQkcNzVd*EIP+@Xrj=K!YTEh3_%qT%4E7lIDCmXW9WcVojeC(UKT@H` z-FkB`*>DdiXi-NW!W`Q3J&fP@_;N)qhe7~c7I6-sp@Kn7>W-jFYiorJJnlurLi{HU ziK!t29n9wVcLFBpfOO?b;+tRN53Y@{FC5ox0mtwbigT8Th*@qRmi|HPh!@Z;@)j+p zDde*Un=F}fSHN*!=GY1ov3Z2F(cLhjds z>)sfb<<%AWgd8o-w^^X2*8P~6dnb6xjrohA8cRhF%1W~+DOBMMHd)%VGZ&gTQ-E|zptsBVn* zuf<^g?IVWWnE05qxp=Wkym8UM&FX%E9V|j>v2zCTxigtmEEfZd{0z|%Kp)pFLCdMzIhk{UZUa8u(ag>2gb z(r8n5@35b4EY{(;-Zy4&c?CTlB_qLcCld3v~gOWLQUWlJqtazB5CHZUefJss;mrqbV2O~c$kl{%ck zhk_nSW~Inkw?G)yKx70Smua{WmP_JHi!MaHdt&*NCTqdsQ2mV1s`Zu~Yq>SefT-(j z22TcmdFWO~vAaatXo{@4=Qi~Wdtf0(-lhW{+u86-^pB%kNkzC^L@#IlW^ZS{e_Iu1 z=ZRG=FB~SEP7L>BE=`D9hVd)SwrAS74*{q}NCq&=oB z4jq4>A5d)PGhsJCLS@nKe~ofUm+zF2SP~|~%6id1GDf|OL(5aZ^|2e5+3qV!9%Sh~ z6F6$U`&f>0Ra2zEAZG{^<*mTDeF@)8;^TfL^( zQY7d(v_mC~ge#*F2VfQZqm6=v_{|yA>5Ix@{Kq;V0mJBYt?liNxpQRgB-0P+5?zAG z7PZdn6i0VWvO3R60uBWCj{GeZ!~|bNJWRX)&-YicBsZ7`HdR6X`*XJ&Y%N;f#v&wG z6>EalA(6UEHhKEpfldDN>2lv~?Sg6ogP&VCqlvL>rm=DBcoK-*6Q?fIJx9lDupOx3 z*mU&!U1|Ff-vN^iIs#oXN!I7XJiYx0Nh@oAAfh`RUuA7DlIi2nkr_*U8qPvxt85%b z4rdpPWC7tSlxcD z(8h;iizyJvYp$BOI?~nb39(u+HPRD#n@T{9`)ch7l&WJlx;XA*9pjxVUjhb|G055&^RosTd?n| zHXB3n@wRxoa|YK+aqcq8nR623cgcN@ltw&_&O!xo=EAv1ppD?TX0yT}(qMX|D@?Q* zP|)@>HhW&Ig}_6rcdV?tF;Z{NWU59X#<+T+ML!gx7vSsHg)MA@l-eCQd(d-q*hcvH zr+WVbvEGNOQ!f6y9?JXOLR`Xq(+_F(C<<+MWV4Zh-h~2*g1-X%YXE5*qkBV^(Al3c z-+oBl&U>vlDbSwAV+-i{L~0Y@JMv*B;xy~nJ8|(O)RfTN96xRzuC@%((K*E?$BLg5 zcx@E18QG2`$#Z^<(wVDlR_8VkyyNybX9)mU%yj)kgoLy3ZOpk_1o1Hrkt~lIr=|90 z0EYR^*Tw$)bdDb!s4x1Ss)a?2G8P>m^=ydG=ojP@WWr0m&hJHFr(;}B)wOND5uD8G zNceUGk?;j4%Qb+QL3bxe7eyav;Z2Y-jlp`%u+h5aYzGDBPO*QVWRn;hCeJZ`f&>}U z9fd>BYsu2P!=A#4-H+*zx_5fE<*BJDW5X_gd~Y3%eBnQ*#PW8}G^aHyB0oWnw~Hkk zcX{!kv4QyM;C$N_HR22kRk`t`<~XwO)3fmb8VNm*>u=!lWt&9+r>TWL(|I*p)0l5= zE-5qxoEry>&MGlBN?GtIL6dGz4OMrhUQLQ0%1o2D7V<)8L+5jpe}kApXJwm^l(*Tm zIVPb;%xU{Cyr`M}HaR6`rW9x0au<2WK6{Wb?U7{#quP{~M0A)fLO9#;PF5=I1bf>e zY1|@XM`Hb$c(%JC8+s|T2Ym)0sfbveUn9YbiNdC0CxMOUzsXK(WN$x`~^x6+(ESdY3$)KAOW_M6JYxl5kP@>YDFl{zlaOOJ4cvDr*iCX?#SDeK+$7Ts+o zd1uB(DD-r-(8cFj2U0j94Tut1Dd~c@=2o@hb{Wb>wY&6dby9T`$r?kGwCd8X(ydNnCUV$i%2rrgC-RVeiSyDU0p(Rgl^iOCRFVYun>#b1*T_Q1 zB0le5fRR!#z5_@h@O_$>*E)ly$M!o-H{gdr&-D_bwZ0hoj4Ji5&*0A+hRrpWTP*%g zE2qY}WgUvn-y&yutkC>B_you!@y>9*zcIv?e!cor*c<-d>qn1dhVqjqM}N(8WoNfA zzS%9NApZUo%R(IMV$`bMrf4#h55LctodupDuI#mVRl-yir5hF@Z08W;jl4lGCKE$= zg!4H*S|4WynP_uQcT4>XspCrw&P{>KPbWXPs*AaT@+UXz;;Oah!|(U-q*auoteZ}V zgz%YiM*JLpJ0=$uN$?7w5cLFyeFLuY=56q3!SnT3uhZqx@HW2N3wn?fH+wCi+ zZqJ7X=v@(DJxrz#hgL^%-^=_I;+$v6$7TIEFO%)UeMH!Lv3J6M!Emh3Ve$K7j~RRH z$wP$TE$YLK@TU|i@BFL**GnnZw)m`w@pV%WqtW9%UGKhryz#gOpq*kI*JNeMXv z)28PxY3WwRokBhj4D7tZ=(iNR7%1i{3KG(Fv8?L)l;Pv>OL;QQ5wda;L8X9itw@)S zu+8(EGyivQvu8PCsB+kpE2wg#lG47IrmdHJb05urO6Yy-ROq>Lax+?7N^DF_#Xv5U-CU8OLR|1?c88{2N0jk_H%+;cRC&3v;P+S@E{xl)aKIxw&`FtF%Qym8~8 z>m==ViZ(5Rf4j)v!w)sZ#NlS|qG(q?_;h`>>cN-ce%2ARB7~=MF~j`mO=br}@2|!H zeG`qh*rhLgyF(kT&EbA7YbgVr1acu%t1!AYH#Y*U6emRM-Nx_N_Ung4+S-4Hv_HsD zWoH$Rmszl5=%-`n%m;9g19yu2rliS+*shzM;wc`IY&f?&`BKQsWbTPjRtf1!y{BJ2 zkkc`FdGGMiKl#1IP?^C=6iA-ha+U+T>hXf*M*1lb$6DuC&E?x_p^$oU%`|3DC6I5K z&=M)AP1~=+R!oSTXgJAYoJs!;jzyAz?k!8xuhD|Qh}9iAJPj6zm8K8-AWPO$a&-pT z%PYP;rOOZ4M8PH>_21>`@Clr&yw=jfdO%`KTX~%o6u>*>6iajPCv~%eIyWjIMWW?Q z*o@TgCHnQcJ+DpEvzzW$A-px1vLZ1KyBN4KHa0yF+Njr7knB)~YP<2T{152dsJmpB zYiM7z9xpk^Q<+sagXJY?sCh8&{8CWC(#Ssva*Nx1y@kj;J!-x{9_qv=vb8>7gGGd; zX#6Evd3jtTe3*}~ac^Dkb`>taP$+K1D)eW>L|}{xVwn-%G~00B8Xund7G;J?cc-LC z3&lLWIK}Z{R7ZN?zSVH{eYL)RkxfH>kfX)hSD+u2V%*1y@YQHQXRNz0!A5`C_3ph7 z`MPkB_6M;!toZh=aav92o&0oGVz+bkY^>KvqB9_Fodh;$uj25Jqj^TVvcZJd;;B0< z>rKrFcUvdB7`ZEh(h}JP;C0P9#g&3?FqsZ{Mjfq)#Z-JHk|d~no*AcvQ5`TRp&Nza z7Tln+;L?-dt$0Djx$vs%v>Warm;uMC9O1%G#$FP_tbn$#Rl|!mvHfNbKPnRAS1IJ) z4Qa{4%Qptc^Ir$U**31MzM~CYuRrl6O<#qBG_#;DUDk=b<+4O1`EC za)ers*sE@B=_IvCx%qi)CyVlxCY#SJhr%^n(>kMI$VY+o=L-sIbKhJ-HZi$Ke~Nv3 zdx=6f6DOP>x7yD!TMmujQ0SjYKk-pV;!m5qMP_>Jm3PjP-rmX-vo#eQ^1MWbX@NqC z*dX}S2UQC1=VTu0oaPbhjDq~mKKYikNC;+f23NbB{c!v@tjX*yJZOq#I@9~lOFl_b zShfU~my<*wus}Dxf%k@HlrI&L>JAVg7*2xypi;J+MCu<}w5@@wn^&`(7l;+peYa=M z6oer(ltVNnAK@|@H3{2XE1gGl=lzjnV~DJ^BY_@(#wQR8o2gt*4}YsZ?BR9CtmUpx z%_i1d*{I+772iF4%KZj&QE;KVE+m#6)?|%B;a9sWn^?r9BQs21?#vUaSh}8Kwqr9= zW30`ATU(!yQSa5E)$W`&i-{Us-7jI96qNm1F@RSz!D0832E8c@^EU^^50K!6drbc7 zP&;B1ql8Iv`%+-PfB@Z}Zx7TKYf)~cbAZqzK0Ji%0jl;`%PqyM`3?3HXhzIA?hJHF z40`Z;^>Z;a5Ak<8nL0!#_iJwaTFaxFvsA;j1X{Q7N4?1-v=1oy5YM8zmM#D)f$OH$ zYTrPqOn(;laAwtTF=MdA>rM*);YYBmpP#I zc&@thI=<(y4&oX2b;h=bi@PMOxfeoSB+aX?5ir4}q3a?{UQ|lt?oU;6fu>b4%0C8& ztB4x!>Kx+agDEY#vO=Eu$`UU>ppbb~q+7V9@XQ}{l7wU?8vNk!S;3bFid5%NhHU$3 z)0tA`zOc6y;ZF5fyt{L?$qZzzV>=!{iLGomF+Yy$ODc?0@5Jypu!JhAkM&`Meq@mA zA-yBS%LQjTakZ~pvaPJBh-hS!z@Mca8B5{IhHGnHAStfGelWcFC!~ht)-y0eQ{;WG?$EK8PA9T*?@YYA7}?<#sQCSzY~fof zsb6HUN~}4=@!~zARp3Xx+wKM3mnflW9l3t}iOXL4MjM>^l9YjsCHWINob?UgEVOyr z=YOM|5QR7)xkJ8=;DuN}eG;IRp=Cojp;V(Qmue^;!~Z>D)dxZTQBUI0r^J}3557-Z zjO?EkXB7&yv|8A6!6wF+WE2$W(ve7)io%K{`)%>553|c_{p<2+Qc`jh>f{#G zb?(P?hQv7Tx5G!%f@DCxsGgnWh&?j+ke9m;?W#b>ie3Tt^pcdFG+k-BfW1@g_&2X8 zB&J8cM|kApDEM&}^#`Wki@vr~i4+vmCt!%AmlhM|UzOU>$?`PKM`ys6y|aS56))?gGrEULUj6)2vQdl2-p$8b9K;qGcq17Gt2&r_8NmfKJT1wXsnM2N9M!Os zZk`5?oppI3kU;m>?H89Tii9~#xOe;CQeKUU)MONGl{&@?df^Xf1~Hzv0$|-y_+149yW3BY_`qA?UWlil`+CUD zvKrsKhC|F|_y{{k{hx^iDKj^s(r^fO%cg-QVp{$N0e)b5e7I#%?PEM6CrrU5=bnq4 zWcgg)Eo61_KI3yzzYExH-G_EXXYs^y5bSjaxxZ}J+GvDfSy${JJ7CDNVaI1XI5W(QqS+VKHlb->`#!^AQCcP)e<*mw* z!~rQzL!r*_lMb(oT_!|m`|YaJyymQsv=a!)Lx7uD!@k+KLAbKqX&Zj9veW%|_jL6Z zA%C;y)!rek2CFWwin#6TlOHV1i%;00N!gG1+6Y+B<<B9p3#k3=!dVH$v1>_wYj8jL6v7 zJUg#8K4FHFS8dU_aG~3q%X8%MeuV30;IU;iHa_gG`U3`;?P(#|MLOt?1q6qOw$$YL zgy>zRnF9Z5u-jl_Qqb5K99tX6eR_Q1e%a~hSktxUT-WI8(^T5N1M)l-@VvjEbv`T= zATHq_7|7o};Nq(Pv$P<0P`F9F`uLQr?bHr&1t?IgJ{%0WZ#|JeJdjNa$-lLBtQ%ih z#aX_Frl**gn&#KG#*StPx{_et-Zt$W6XupqONptzx-ca8lin6P`y{@?>b3mipR1tB zU_0i|#w8a8`Q96}YXpL;*X#JFYp;A*NaOk<=dP+j+g6WziT8)|F!=5tn}~FC09)#x zkbtYow_svXSa7{|*K&8WdQtCn#fG>|Tr7Om?aUc!eTCf__5Ov=DZy)+Hzv@zb=(v= z4DG4CuM~xEw|1({ZCDk%o!ePk{HO>dId!X)D+y%h%sGb*k!T)(MfOdVU`AZb^WhKa zV;ew=cbz$QHQL0$6x}XWxI|Cbc#Q0L_IF%*H8J=E>tF(~d{{F|;|=QwMa1V$j`Zt_ zrb>H;4FLo+H~5i)h*wM=OET4I^2fv92NMz%6nPU}VypW@xUz5Wc1saaBv$-G zdh+dkLwBl$P62va_c$UV?}=n_OK|x(Q!#RS{dzyh|9<=^IYUlVs$9YuW=I5_4*dop z5ampm9LYsDE~DXhusU0jjy&JpEI;eI!(b<1l}v5>)hnI&t*s`F8m)jWhdCPAAgVJ! zgcxxOYq^<=<$pLdtUENz;-hC-pq*g{60^+dvJO*zm(G-cT&VkD^$Z)=q@xbyvIa88 zZrAcDHXm1ehn>7YZ%$-uPqEsyUBy6eCP!*oGMY+*JDwf>5|p<&W^quhkE zH5yqff>>SWeD26lCcs!B_w2*~%Ju_oWi_(c6K!$TH3ORA9wQER_Ma8x?$6?6 zT4|8n8dJTOY)pu+)E{NfFCfT#k+zU#eAln>(fT4SqTf2R#qvOr+v=>Z z42Ru?ih>u3Cg-W|H>|4R0v_l5H^d#^s(pdE)omva8a>JI^ydQ2jjhw*J><*S8q#aq zz-|VaRtze3t?+8}56$QcxKk+Z62T z^6)Y}KKKnx{%nIy{_HT7tg5tA-0Qug2jv%^FT~=%Z%z#|+-_}1J@(&uwqM~xc)eyghq+m_y8^SY&~+N8Wmi5UN?B&`9ufe2~Ieb2x$^p)k9ukD}c|{bu10i|-yRUKP6r+Z{`hRc|;|6l!PF0@s>NB|b zf54XFc*_yIIRvo|yRJRPow}TvWC!;jCEpXVB+<`VD4KJkU2~%udi?6#Tr6~lea!H> zce+2m@;E$*V%cHRp6SX1UKRz#rYL`E9{qWaXRR1%OZCb1TCM?*4EOLvu36jRb!ul8 zQ5yG8_MrQ9EHD1s;0^Q2k)Z%u(QnLz;d4ic`CaMn@3X$ialUrGI+~j|s`Pl!+2Xb# ze)&!HkL=owVI7&pdM4#ljd3YX`e7r_<3>Uzb$+b_os>^22I4s(e64Z!E_98aMaj;S zHwZ;_QV&0Yz`66(^dD2#8_c~~$as~`%b&_txx>u~(3NhFOynp$AoJ&P)U>LuFBdmo z?$GC$AODz1Kv!Bw_{<4x6DXV<2|@=GZAOY*WI`Ig5+3XYNi=vA=I$`=1Xy`_6U~+z zc$g4uZx4OYs!RLpEk-g1YwRhnC%4Pj_DVKyW?^k4-!Gm%o?Q?w zv?^9XW7P| zo2n>v7U0(4V95V*W{y@}^Eeh*tP{P4LGAW__40nq3o`hZZ)Pr^u|ly?B8D#VPI-C%9AGf)$Fp7k3F(+=3R0 z7l)!DxKrHSi~G&aMU<=uJk2{E*kDCSK|SBP;rNKe_a&kh5CGd`bfOk~^4?fwL4_cslG=VJz}H#VXEIoW<^Y|I9}w(t7s zn38|AInZDzV8~OSER7^$2;E+ct;%BOko~crvdD(hEY_n|Mo9s&F9*4y@ zsEw;OLI%(n+DJoGdNKtwfEe?x!zWEH(+`c6>?{`d(JsVFDl|*$MQy&gU4>B7NsD=X z83Dg&n{sxt|OnHi^ZND{e)pRhVYWj5oup$Vyxwja*Fb} z<%=2tRB@myVTeC(m!tDhU@1%Fiq!tgD1(~|Uk3N^7njFx%N+q&^VO~4#fs^5U(N6Y zFU;Ql@<0ie0ttnzJkXIlbtR+$C3!*-+0eBr zddxHB_3^+%MSxcR!U|J}G>26#>kVlvD18lfoPj)|Naa)hnItLgxxJx2EPPC&@!|?1RfW=PkA-r%NpIFPgtY zQ+Mh!a?+*2hsAAcAu$wS4B|THUY=;pD!LojIQjf6g0oJbwjxekfk4AKzzRMhck>?% zl5_fM9a^yEdDMw=Gb@*1vNbcSl*5~MoqR3zJQ)i?2;u2`#fE-WbrAv4y=R7iqWeaA z-UZxD^Lx7=F+pau>WJ+@JK*{pYZ z%u7_GdkZz4u5T>{b9=Xmn$&W!fEK*knEYH{o|(n(>!IH8(Q_SXUROe-kB|ex6k}+P z=jy=q(OjL?fuzpJC>?BnK=^O(=fU8~%KUqu^80hnf)QJQ3P=7QfuxsMS@0VS^r(A6 zjxRlhL@Zy7J7eLWsV{tU4VBrc^C{r!>!QZ_NwSHp3nUS_V^^(! z`Av?airNt_qYTf?NA402dCK6{3wV6%5>y`z)GRprFe_)b;8%u9i)XKCmHZ`9afn>p zd|24PyOk};sfTd)S99*sS$l-RJgfCe1rckmcnB#FPcnqv=iynNfm~<$LsGX2w%_)j z6yc~R!398q$0{*9Ty7N@vwI006AQ7O<%(4#9vDOU52#CplQ#bWbv8)FS*CKx)m=V` z6*ppjzrA{3#jHjKL)XPE0QtP0eyBhQ4RFo^s6@vLPp*9u`S))sX~}M1NWc3c2)mR& zx$-H@{%MXaMKwnWXw2??@4Zb#n3$g43n65=u=18py-4a#43K9K_R?TFi!NDQPE477DTk0XreC- zMtb&VHX_DZA{aUmzyVK={sWg;ZmP0mVJ3^4no zY0Yl65$#e!+L}bkSbj@}cAs;{OHifo$rS_tqmoE{I<|6Hypd|n?YcU)8<|><@swg( zo#{$dCG$j*X)Igzu$=G(S=*X-bfE9a``Hz-WeJu{OCh_?C*IT7Aw&_U72e7(0?DNP z$b{Q_;H-4jfJsoaH5+Jh$Z`ajV0u(lVuov&E0L0zmE0{krIGw2m3P1!B+cnoV}=_8 z3!-r0_NoiEkc)TXy5zH3d*ThayEbUCyvA3sCgN`Vn6TU~@(+VSQ^Z0HBW=7GKr}N@ zbQSt%=e963r-5;lj+ZG!@SsS$t(B`gF+5b~R)o6qn)UUrmf z@6vT}ScPHIN}40#ZksN+V9tSLE>x>B=G5%E$KDDjf|i60NuQfq`~j(2q%-5BDn!5^ zaet~LspElM>s%*NGHR?%sF&2tB0E929mH4lhmE#drtFvt&`q#{xf*F4Uzr& z_~bR~)$_aG{%%V5ZgZY$z4Q9#mqsr>?ih@ZBjfb^T|vRHX{HR_p(&#JjEpSRI*{c? zbUB26N%o4|tvI9rB|d^xzAl{jYsvlgC$UwgB)&5Ssyq0r!2FqcYocp&qf!Qxv(nZfpLwAW592R8q#72Y)qPFrq*H;EKt(Os}(_ZLBn^&n(ttOrplI_H-_N z+mZeEhM40On%u6rA|aI*PUG;d*yNu5HgLnZ(`kikXiwQ%U!CgS+k2asSX0Cm0fS`3 z+S@}M5u4(D(+9!zTIB4i7H-HEr8)2T(Nx8sC}67#Yo zYA-nDu&Fjzp1(EI(X^8eGkv*0?(}`dCkE|*8tyH?P5eaPNBR!j*J}_GNA+| zHOx?f({85F7|+`73B5pz{{e;-1XqRb?o-8~2kG>H9Bejzas!1;?bo;z)}v2QdT=6G zQbB7}*xJuEm-&vSx{J&f!Yq1lrI{b%s8;7Qc5125zHrfVLU@Vy!HLJ#F~$cV zeg!-M*@{toI_M%=TMgz5sN*{Mforqd;D&07O5#Gt>hy5=Y(mD;`0`M=)lNO1vZd8I4S z0QjaWozEaF9)A66Z6F29R2FGCFg`Uf83U8v6I#jM&O^3A8ugd3tj18wqtlI5FHdn)W_{8~58g*6G>Mwz#RNDd;p_W%!M5*2@q=(6dvVip|4A0r zW+%CBy_FfdrVCs;0i+d#Z_75}lxqxAW?Qy4`RQ(_vZu>Zc=3pGeWEQTV87gY{>;R3 zenVCt(QgU@oTJm*tYu_K2IH@AaBJ8DzE~y}hO`c%5X`}^jvF7Sqykr^hi%UgYG~jC zRccHr$hvKKz?D*(!APnng7^SyYF%ET{W%Nq@}afzGsu>Tsz$n%K$Szy@N@-SFon0{Kz1E7YI*rZg~vrc4fFE zZdgy8BkrylDJ3`gN~e94g_^3gZkD8NNPSS=_P0;;c4ZvV z(;l=n5Vs5uvlDSzTQL-734vK?g(QP0Gb?bvuBx8IPnLXJ`g=1O;pcZnIJ<(^ zQgCHXXDDl!1ix)-HJ_~<_)=2#i#73fiq;z7bngcNWd#^5U00}W zjBf4C?&JMt2YIuFJG@;jnap}J{KS=S#_2Y-x;zLqKmq2oWFArbL_VIjC1nj;e`*rQ zs@&dx$^MZE)Ia61N;MwN6y|e&rQ~;6iIx7RF)B?MW;*IiA!G=HvL)5O{t8`m`b}+V zSDf|h1=l%>>92LA^)*|Q^Im2@EBhD?qs5(Hu961)&>n5|$!=0%Zq*HSzDmDm(_WTR zP)U+o2lgYmuJ)eic(ZH5-aiV}RV1ZxHf34|$n0#vRQ*LP@4_2k3p5Ip0(}HI@INf|8 ze;Q9+>e%-gu>zYfq39J)w2mhdaI!g3Ky^r=_~nK1b!49U%P1By*}wDTM$lke#?*q| z$asQx6b*P4Ndavw;f%@vVW*n9FKTD6~ zUd5}Cz_~=s>5s-<+~RjjzAX-|&re1Aw$1?MjM2q5U5ykn&oTAWU)gVxoG5&THBMjZ zt1rSL(m0KT$8We)>6~HnchkZpPAixgX1t-3bCTEPn(eQ(F{bwSSU2luU-z2zOc(i3^d3XwQY`(fQY;EnIJl+Z1h>i-sdhx%WvEW?( z3BkEM?=4a_@jJ&5c{(S#Uwn1g%c#I;wjfX67w*c>JfJH};*@8$W#Qv=y>@49Q2ZM) zWT^!X?M@k7oY@ z^>?K9)I&v@2cSpezQQ)rd1|H8Hw;kNT5tpaV4-Z5Wb@0v!Gf1DXkoqkJmS2JS@-9k zF*YUyeH|#@acKvy8geD?xvq88GcncCB>WuF{8Vb(du>1NrLrP zo=(}*BgpF?s|t!0o=_r?DCApnviA|6nnP+7xo3-h-m&N2u`i(v1K+&O^q*~pDm5z8r(iEX6`kttYYn zNImLa<-IqSEU-BCYD%hj_P4RZ=manQs4vSFW*i2PLRVEyy?h5jBR@t*WMVk?G52TK zMNV~?6WR_u`^7eH?{M7!>ZsIQv^cH&JN8EfwBOg1xT>oaJ9`{VZr?7@UI6!P6= zqdRGFu6nfsmK4yoP{2YWW-%*U&QRP>ILs$BBQHudZWccyb=Hq_#9&B6`yL;z1pE~M z4h0NP#-tQrTx|q0_AL1YWma)qEKjRmjA*J}UQN@LT6;ZYSbN{J?0KDLlzQJRSbN*v zGGV*tUY_n~5@RU>nBohkT2fdC865Wiy%++3v;hySTnsyAy72yZ;0S9zv9PIuL8z_`Zt;=XEi#*_-iYj|oSCAf{i^Hgb zJpN0{%Z-5Dub%}#L@K? zPs5}eB`Q5&tnJhpFlv@}L$faTi*zXUV8g^^kyupS^`|iS9sI9oVA?2XIo3KeOfE(u34d=uXZy~TpjR09$YPxVOS8?imT$h<*%Bk(K ztm*`M^v@!ZAZc5V^`nZFmBew~R{pfqq2a%&%P(eHoJXMTQ!$9L5CA>`8gFN5aXWEK8DwdCk8G_f!sHB4!5Du%TaO_A4pG3J2DgsqfRX z&=*g`GP2_;LX)r%uY^!-ZRR4S$J{^LmNAz__g!VkjO68oR2u(n`Jzb#0HS8pGZ{9A zUvDv$UeCLngku@DIdR2sgD`7hiF4s?U73i`t1AH>x8FR2ZK?P7q^vcut9=KDrEl>! z%AOeteih+Dk`4RrKiq^$RCAU+1YHM z>8lWj7svcCM0X${dhIQe)dbVm;X;wJzZd%&s^5a zf1ZibjjInbC!q+3_)RGe=`}!7iY<#%*hw?t6lk3dv0@2BGH5zsk>+R?DAS8g2u^mO zvs-QyEL=NvS?Kg{$JD~Q@@eRi1Z!wAyu`JGf@gq`%qj@Z+HZFp+2AFOgblh*8*#94RXBAk$9Vc+P@GX zHTzzb{|`(r1GK%X;#Xz`48Br2hLwX58Gt=IHeWgkoW-k3V<##f zq@+fPenQZjPJB~6uNc^cdY!$ehsF7}1qA6&N*3+j@<&pR3#x?jJ^Bne`i8#puev-u zG~G=NSXO3z9Uv6eDyq&iRV9gw9>^~=i>xQ;GUdxC2P$pz__;?p;AoH1jHLn9iGNt! zCb)v%2uDRZgRJGU!X&5_AYr*;$Y-Q$f=aaXrLn?}ij2Z?5yRwfsv;Ko0|u&J|JjK= z6}@gXoy|L&pNlAWncIv7_^41qNic&K;_=TXo~9D+^2GhuD+3^7g8mnd_m{5^i35qj z<}-A5`AmC9HV3rwMxjDt+x+7*HQxgH!QDOi&pvaN?ct90gCAD83b33dc(nV|52y&xr*nEB2LZg+LSrnsX!_~Rrw=$0Gb?DW zh0vS*k;atZ^^}>%O8@av_CBASdWyx_pHx+rinviDYyTFm=_rCp0tX{{k++V8c4@uQ!bAlgK*0!DFq`l@p3PB|O>+`T67(AL zn5M+cs)A`Cu5$rWo0Auz$ll-C2)i9UR^mz>mcu>;5i(3Y*7vatn><)K0K9i$7e4;F z&9FkGW^|+u_==Je@D(K#nJ_&*Ly90#-(VCa40*S=5 z79fX8a}mU#oH4Ks>nF@}WX_dzz94*8uPNVjlxPv}IeXG0z8^~kPbUCa~IfpLjZr}n^doYUPDVN*PBfk)RL1H zqp!5c2VoaY2F+H;yr#(`Xo=iC>nq9a=~R}}v$mgL|1#juqes?pi;g&5^ru*TcvlS% zhvVovrx+J|rl{_?7hA&SbE@O-yWa${;r`h>meQsjM4|gCAitJ0dK_@+ZS1uXcA?4v z*KFpvyFDTzrEtQh-8)G?g8I4@Wf6JW+2jOoR$1CTA?aH=rcDs;t;Hq1HW$7f(#e2R z_G%k(L)jucUqizNoeOEwIC)|W4`5GdXknRG$BXKQ;+qXgqdz(%|5YB#NN$iwd7^z$ z#((H1;r4HA6v}6}uTEpc=yZKVtmeC|5zUXM%5ccZf7x?z9$)W}HMAv$cG z=-aN#CjbD5NT|iT7_~=Tx$#C(onbljoPe2(ZU~t6IZf6O6DF@gMw+Yy0ZDVDST6MD z&ns(|%C1LW#SlnOzD43udw&?I4Nh#la2pcJ+#k(^fd;5bX@KE z0v~6>Gj$Ddc^6ejoLD{p!HFE(C0m8aY7M3hhK>7=Z_p14hh~Fd_C3UTl{WmV|| zNSBGIb&D}`FMo_aKz{#8Y9vlkU|$B^JL!!ccwz$0L`eo+55<61r#@8+*!w}TdD+;< z_fb8?wPVj_zslAYFrX(t88<~eB@oA2>a>o(0(lVqXv|2ZOa}n&Taw01231b?v)_|) zxy7ey@hEq!2h{PfEn;#iyajEm3st98mj4}k`GI=YmUXtwnbRJMgioL$-#}iAqoaAh zTYa~a&^1h#;FnJQbaW~5Y$jTYLU%{#J`yA=DaF5x`_QB}v)i2C1adjZ1@| z)`0cP2r;V_H}Td-gbkS~9wOzFof+to*AdLq2J7+MpiW*XtGC0XBaI$|D@oNcW7w`a z+2cAJx+7>qRyK*J8^NEzI!xlX{7!eZ^depFKj#pGtcvW|$CAIMv<9gY4V>U3PHZ_0 zMnuEEoM*M+;W_y8oPmSpvbE%g0v~Uv=;*kUw;QyyKBs|v9#90_j**i7lzvK?E5^eD zg$HVUmv4YAXT%u!Xt?d?Cy`{6P#qfyr5{pe8~qweWttxc7vKRRVu0GbgqH0RMFMnz zyV;-gzck2>{EcXj+7@hbS>b<;dVM|-zicw;2yeW{e?egMe|8WN6A$PLnTsazx;N>9 zQj6cYzdY;+{k|UP99x)?M_VwZvFj&k-Y5{?Q-#NMyxbYAZ3q+ObYA**-p+EIuaGlv zv-J|TWyTaTk$wNx1$sn1yD&_SmuMn*Y`5HAc~o}DB~$%Qb3>|Ii?{c%F&fC5eH3;< z@c$tR$;bPZ{Ys+zEh1g=@Ij7`5(P@Dch_yHe#a}GeL zELA;k!IdQ*rY}V|tWEayV=>mVCk@YM8VWMKUmhSPcreD%4o{(GpmK~3QEOR3p)Kw? zK1D6g7dD2?t&_b4)bI$pu9_;>g*eRAh#^Ixy}f(Z_p7J1mrxcTC8iCug7t|{1sRF- z7*;VnyQTXMJ`N2>*c7I!>ge{WJvjRa|dy?FchqtlDRgO`m+T}aHXC8R} z`Z8?wz{IIox`h(9U#kY5S8|TW=zf{Sbw}fc)-=jRd$3%9<;!R2d zZ+>FvPv+bVmIs5bWTs&w9Oht^e#m^m**Ce0zpzdf#?c|K3c4vpok^(G&^H$XTR6tKf z`0Q6W6G58uLc!9x-B93?TC~Hgn9pnjm#S%Z{2_}VTZbN9Y#k@7X%i`{^qUUR)9DTH z%@N%oQ}FA8A=5NRZj)P5;0dibCwUq2pC@D9HWk6UZtK^r&X^~N!2i0V(!xhp{x+~B zoC#ZTa#d88ijo#XpEXZ^Rf^0_9)BP8$s@^iD(H=ILV7?%XkmqohKP`n5D=Yf_tL>Z znCa4Sr=Y_tj3{KDd?BYXv;mqt${gmBA{YwA(QV^nZo6-FX z{+VD< z0!9H3cvoM`MTSax8FhS2n&=BDFe~7UZ6h5*L*QtbnLKuOcClGWn*mjpmuFv7*7ysy z$Xqt_amW{{OD6lEh2384%2jDmN_sN!s?CZT@@+ zc@`5=3C5rL@QKPuUnoO1GsYLOQp9RctmPMVZV)0 z$GV68S98&@_6tw9a-P<@x>VaN51ka;y_v(8%z_Zw&u!HFFiNElLYJgsI9VwBm>VJhRK>(_(vk z%Z|_f30Ml{+R<0A8nLtmQ`Z|uY&r~Rzlw2mIsXV@%L~JB!J1H`$I?boo|@n}aI25{ znFa`%jbE_=sK*7=f4pvsx)LBw9GE_EzjF2MaL7FDxbuJaPulUFB`F()tb+|GzdEn8 zU@7rJ;|U=fiZdzv7n%=|(&Tk<UMaoR<027G4^tL(8gY!gWr$cjoNlhzg%Y}v6C zTXhbi`xRB?Yi|Gdo2^oo`5)p^IV|Bxzf>(=Y&B0B^ze$HTcjM(6$+Q3P(tJ>jl@d~ z*woSZf|dkX1=fb+(ZK9XxF;>shkTe>t4hPf>B;O!=O}x?(iBzi{OF3 zshPnilQ3H%hjCrt!(49f%_9Re-cTQ2-*c8#ro)KA?ZwVQf>0pZ}$-U&se2N;6x>} zTq^%#-19v8IaZ|uZ^E}1VClZCou*)oI-DU-Q60m@(t;;WTqv(?A8j*A5q)gn@Q&LBlv3LjluLf#_$jVW@=JJ+@+g|ttGRx{Z7Z=Y8=4TJKXB8&SIw!6*tQ(`QJ;uP@&(8{ zJjg&IQ5>%OdUo4>?SQ5H!kKQqUN%e;DkJqFM?%suKCK$UWRn?ODE;%)Bo&0%XAQQh zPx`5uWQks0h4@;|pc2mWcF%i_END^HQC0Yng$PA-s?YJAyAMPS*XTq-g_lnqFFrnv zM-1<2%$ONld*hh;PZ@VG;rK`)s}*tI>M!Xv-A%|_;i8^LY_ zJvp_cTGb&M%Ns2&%m>%GJ1~e8n1)VO-R*qE$%uI9-E?i68NBJZBkxYaNVYOqN2Sdl zchz9HOBlQpz4|C06o1#}rm$;;=h#$e@kZvS#rT*kHZ_K7yP=dr53g{!on4{i9G;Hl z<~tW)j$j5)j>8CsJIhrB;9TOH<8t`$T!$+kf=pX>y^IB=@i3Of%yW%tYYWA!o@{ARCSp-9)QNe2 z`Ti&?X1~MGo8q4lm}UFdSXVbQ^`3co2;+C+QN%X{zl0Su<;ZzuGiu(=_1J@nm1yLu z#p|uCu3Rjh>gLqyj%81?i@|2v>gV~>{F8#X`Mw}8OI3-nEEwj`C=pTV9y64ilY-h` zxxeMzq|3l58|7aDyzpvoRkt$>nWo#A$```Einaee(_d(nxBS3W&CCjvOf zyE8!~<6f_wwqzguvqfgRH@;f)yr0@LkICymX@v9+7m{S(1LV?IbXvs)wu_ zn(2aW1k^c{?gZsw+!ujK;uhL4d%9gEMj0O&Yu1ye9RWa|AZHP9X^;2xRE5}1J3nNJ zZ`ZP!sd#+czh(;E*%=v8we-+Gu9MmTDN@A;$=A?hVqDJNrKBca^ZV_xju2 z^_c5gx=K~K)O6Mz#Au065>M;^-SsT<2_%Oye9=_UY!y6T)TzLk2_BOk%o}XCMP6>_ zBbfFUeojOnE%Fras9N≻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 WE2a-5*t35Hcqz+k$d$;N2K)~te|7f& literal 0 HcmV?d00001 diff --git a/docs/eclipseFinish.png b/docs/eclipseFinish.png new file mode 100644 index 0000000000000000000000000000000000000000..718b2926cd8f993d594fdade55559447d6bdc235 GIT binary patch literal 206083 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%0DsDcHA|0dM2Iwh_25`wf_YQ%}-`#0|t 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�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