sync: merge microsoft/vscode-go@2dbccbe into master
Change-Id: I8d6f2f0e99efb23f3d701f39da75566f7d39186f
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..05009b8
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,93 @@
+name: build
+
+on: [push, pull_request]
+
+jobs:
+ build:
+ name: ${{ matrix.os }} ${{ matrix.version }}
+ runs-on: ${{ matrix.os }}
+
+ if: "!contains(github.event.head_commit.message, 'SKIP CI')"
+ timeout-minutes: 20
+ strategy:
+ fail-fast: false
+ matrix:
+ os: [ubuntu-latest]
+ version: ['stable']
+
+ steps:
+ - name: Clone repository
+ uses: actions/checkout@v2
+
+ - name: Setup Node
+ uses: actions/setup-node@v1
+ with:
+ node-version: '10.x'
+
+ - name: Setup Go
+ uses: actions/setup-go@v1
+ with:
+ go-version: '1.14'
+
+ - name: Install dependencies
+ run: npm install
+
+ - name: Compile
+ run: npm run vscode:prepublish
+
+ - name: Install Go tools (Modules mode)
+ run: |
+ go version
+ go get github.com/acroca/go-symbols \
+ github.com/davidrjenni/reftools/cmd/fillstruct \
+ github.com/haya14busa/goplay/cmd/goplay \
+ github.com/mdempsky/gocode \
+ github.com/sqs/goreturns \
+ github.com/uudashr/gopkgs/v2/cmd/gopkgs \
+ github.com/zmb3/gogetdoc \
+ golang.org/x/lint/golint \
+ golang.org/x/tools/cmd/gorename \
+ golang.org/x/tools/gopls
+ env:
+ GO111MODULE: on
+
+ - name: Install Go tools (GOPATH mode)
+ run: |
+ go version
+ go get github.com/cweill/gotests/... \
+ github.com/rogpeppe/godef \
+ github.com/ramya-rao-a/go-outline
+ # Because some tests depend on the source code checked in GOPATH. TODO: FIX THEM.
+ env:
+ GO111MODULE: off
+
+ - name: Run unit tests
+ run: npm run unit-test
+ continue-on-error: true
+
+ - name: Run tests
+ uses: GabrielBB/xvfb-action@v1.0
+ with:
+ run: npm run test
+ env:
+ CODE_VERSION: ${{ matrix.version }}
+
+ eslint:
+ runs-on: ubuntu-latest
+ if: "!contains(github.event.head_commit.message, 'SKIP CI')"
+
+ steps:
+ - name: Clone repository
+ uses: actions/checkout@v1
+
+ - name: Setup Node
+ uses: actions/setup-node@v1
+ with:
+ node-version: '10.x'
+
+ - name: Install Dependencies
+ run: 'npm install --frozen-lockfile'
+ shell: bash
+
+ - name: Lint check
+ run: npm run lint
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000..eff44f1
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,84 @@
+name: release
+
+# Daily release on 15:00 UTC, monday-thursday.
+# Or, force to release by triggering repository_dispatch events by using
+# curl -v -H "Accept: application/vnd.github.everest-preview+json" -H "Authorization: token ${GITHUB_TOKEN}" https://api.github.com/repos/golang/vscode-go/dispatches -d '{ "event_type": "force-release" }'
+on:
+ schedule:
+ - cron: "0 15 * * MON-THU" # 15 UTC, monday-thursday daily
+ repository_dispatch:
+ types: [force-release]
+
+env:
+ GOPATH: /tmp/go
+ # Because some tests require explicit setting of GOPATH. TODO: FIX THEM.
+
+jobs:
+ release:
+ name: Release Nightly
+ runs-on: ubuntu-latest
+ timeout-minutes: 20
+
+ steps:
+ - name: Clone repository
+ uses: actions/checkout@v1
+
+ - name: Setup Node
+ uses: actions/setup-node@v1
+ with:
+ node-version: '10.x'
+
+ - name: Setup Go
+ uses: actions/setup-go@v1
+ with:
+ go-version: '1.14'
+
+ - name: Install dependencies
+ run: npm install
+
+ - name: Install Go tools (Modules mode)
+ run: |
+ go version
+ go get github.com/acroca/go-symbols \
+ github.com/davidrjenni/reftools/cmd/fillstruct \
+ github.com/haya14busa/goplay/cmd/goplay \
+ github.com/mdempsky/gocode \
+ github.com/sqs/goreturns \
+ github.com/uudashr/gopkgs/v2/cmd/gopkgs \
+ github.com/zmb3/gogetdoc \
+ golang.org/x/lint/golint \
+ golang.org/x/tools/cmd/gorename
+ env:
+ GO111MODULE: on
+
+ - name: Install Go tools (GOPATH mode)
+ run: |
+ go version
+ go get github.com/cweill/gotests/... \
+ github.com/rogpeppe/godef \
+ github.com/ramya-rao-a/go-outline
+ # Because some tests depend on the source code checked in GOPATH. TODO: FIX THEM.
+ env:
+ GO111MODULE: off
+
+ - name: Prepare Release
+ run: build/all.bash prepare_nightly
+
+ - name: Run unit tests
+ run: npm run unit-test
+ continue-on-error: true
+
+ - name: Run tests
+ uses: GabrielBB/xvfb-action@v1.0
+ with:
+ run: npm run test
+ env:
+ CODE_VERSION: 'insiders'
+
+ - name: Publish
+ if: github.ref == 'refs/heads/master' && github.repository == 'golang/vscode-go'
+ uses: lannonbr/vsce-action@704da577da0f27de5cdb4ae018374c2f08b5f523
+ with:
+ args: "publish -p $VSCE_TOKEN"
+ env:
+ VSCE_TOKEN: ${{ secrets.VSCE_TOKEN }}
diff --git a/.vscodeignore b/.vscodeignore
index 5f7ffd2..a0b223f 100644
--- a/.vscodeignore
+++ b/.vscodeignore
@@ -9,3 +9,4 @@
**/tslint.json
build/**/*
docs/
+*.md.nightly
diff --git a/CHANGELOG.md.nightly b/CHANGELOG.md.nightly
new file mode 100644
index 0000000..f93a946
--- /dev/null
+++ b/CHANGELOG.md.nightly
@@ -0,0 +1,4 @@
+## 2020.3.x
+* Set the extension name for VS Code Go Nightly(`go-nightly`).
+* Pick up the pre-release version of `gopls` if available.
+* Disabled the telemetry report for VS Code Go.
diff --git a/METADATA b/METADATA
new file mode 100644
index 0000000..71e3b94
--- /dev/null
+++ b/METADATA
@@ -0,0 +1,16 @@
+name: "vscode-go"
+description:
+ "Fork of github.com/Microsoft/vscode-go (the VSCode plugin for Go). "
+ "This fork is kept in sync with the upstream while includes "
+ "new features go-tools@google.com team is developing and experimenting. "
+ ""
+
+third_party {
+ url {
+ type: GIT
+ value: "https://github.com/Microsoft/vscode-go"
+ }
+ version: "master"
+ last_upgrade_date { year: 2020 month: 1 day: 8 }
+ license_type: NOTICE
+}
diff --git a/README.md.nightly b/README.md.nightly
new file mode 100644
index 0000000..0064e7e
--- /dev/null
+++ b/README.md.nightly
@@ -0,0 +1,27 @@
+# Go Nightly for VS Code
+
+> ### **ATTENTION**
+>**Go Nightly for VS Code** is the insider version of
+[VS Code Go extension](https://github.com/microsoft/vscode-go)
+for early feedback and testing. This extension works best with
+[VS Code Insiders](https://code.visualstudio.com/insiders).
+Go Nightly contains previews of new features and bug fixes that are still
+under review or testing, so can be unstable. If you are looking for the stable version,
+please use [the stable version](https://marketplace.visualstudio.com/items?itemName=ms-vscode.go) instead.
+>
+> **NOTE:**
+If you have both stable (aka "Go") and nightly version (aka "Go Nightly") installed,
+you MUST DISABLE one of them. Docs on how to disable an extension can be found
+[here](https://code.visualstudio.com/docs/editor/extension-gallery#_disable-an-extension).
+
+> ### Difference between VS Code Go and VS Code Go Nightly
+> - Go Nightly is maintained and released by Go Tools team at Google.
+> - Go Nightly is released more frequently than the stable version.
+> - Go Nightly includes features and bug fixes that are still under testing or not finalized yet.
+> - Go Nightly may use the latest pre-release versions of tools (e.g. `gopls`) instead of release versions.
+> - For now, Go and Go Nightly maintain separate repositories. Both repositories
+> welcome all contributors. For contribution to Go Nightly repo, see the Go
+> project's [contribution guide](https://golang.org/doc/contribute.html).
+> Go team members who has signed the Microsoft CLA will send a syncing PR upstream to
+> https://github.com/microsoft/vscode-go every two weeks.
+> - [Here](https://github.com/microsoft/vscode-go/compare/master...golang:master) is the full list of local modifications.
diff --git a/build/README.md b/build/README.md
new file mode 100644
index 0000000..cacccb2
--- /dev/null
+++ b/build/README.md
@@ -0,0 +1,88 @@
+## Continuous Integration Testing
+
+Currently we are using two separate CI systems to test all changes and pushed commits:
+Tests running in Google Cloud Build (GCB) and tests running with GitHub Action.
+It is a temporary setup; once GCB fully supports our desired workflow that works
+with the Go Git repository, we plan to use the GCB-based setup for CI.
+
+### Testing via GCB
+
+This workflow is triggered for Gerrit CLs (chosen by project members) and all
+the commits merged into the master branch.
+Note that our main repository is in `go.googlesource.com/vscode-go` and
+`github.com/golang/vscode-go` is a mirror of the Go Git repository.
+All PRs sent to `github.com/golang/vscode-go` will be converted as Gerrit CLs.
+Currently, the results of the CI Run are visible to only project members.
+We are working on improving this workflow - making the results visible to
+public and easily accessible through our Gerrit review UI.
+
+- `build/cloudbuild.yaml`, `build/all.bash` - define the GCB workflow.
+- `build/cloudbuild.container.yaml`, `build/Dockerfile` - define the Docker container used for CI.
+
+Project members (currently restricted to our GCP project members) can manually
+trigger cloud build and test their locally made changes.
+Follow the [GCB instruction](https://cloud.google.com/cloud-build/docs/running-builds/start-build-manually)
+to set up the environment and tools, and then run
+
+```
+$ gcloud builds submit --config=build/cloudbuild.yaml
+```
+
+In order to modify and rebuild the docker container image, run
+
+```
+$ gcloud builds submit --config=build/cloudbuild.container.yaml
+```
+
+### Testing via GitHub Action
+
+This is the workflow triggered for every PR and commit made to our mirror repository in github.com/golang/vscode-go. We are using this CI to run tests
+in the platforms which GCB does not support yet, and allow contributors
+to see the test results for their PRs. This workflow is not triggered by
+CLs sent via Gerrit yet.
+
+Until GCB-based CI is ready for general use, we recommend contributors
+to send PRs to github.com/golang/vscode-go as described in
+[the Go project contribution guide](https://golang.org/doc/contribute.html#sending_a_change_github). The results will be posted to the PR request.
+
+- `.github/workflows/ci.yml` - define the github action based CI workflow.
+
+## Nightly Release
+
+A new version is released based on what is committed on the `master` branch,
+at least once a day between Monday and Thursday. If there is no new commit,
+release does not happen. This nightly extension is a separate extension from
+the official Go extension, and is available at [the VS Code market place](https://marketplace.visualstudio.com/items?itemName=golang.go-nightly).
+
+The version number encodes the last commit timestamp of the master branch
+in the format of `YYYY.[M]M.[D]DHH`. For example, version 2020.3.702 indicates
+the extension is built with the last commit committed at ~2AM 2020/03/07 (UTC).
+
+- `.github/workflows/release.yml, build/all.bash` - define the daily release process.
+
+## Sync with upstream
+
+### Merging commits from upstream
+
+This is done manually by project members, probably before each nightly release.
+
+Once we consolidate the two repositories, this process becomes unnecessary.
+
+The merge script will create a Gerrit CL for merge and issue the GCB based test workflow.
+The remote `origin` should be set to `https://go.googlesource.com/vscode-go`.
+Make sure you have access to the GCB project and `gcloud` tool
+is available.
+
+```
+$ build/merge.sh
+```
+
+In case of conflicts, you will need to check out the cl, fix, and upload the
+updated cl again following the usual Gerrit CL workflow.
+
+### Reflecting commits to upstream
+
+Once the feature or bug fix tested with Nightly extension is stablized, create
+a PR to the upstream (github.com/microsoft/vscode-go).
+Please make sure to include all the gerrit CL numbers so the upstream code
+reviewers can find reference to all prior discussion.
diff --git a/build/all.bash b/build/all.bash
index 19ef780..6b72279 100755
--- a/build/all.bash
+++ b/build/all.bash
@@ -55,6 +55,33 @@
docker run --workdir=/workspace -v "$(pwd):/workspace" vscode-test-env ci
}
+prepare_nightly() {
+ # Version format: YYYY.MM.DDHH based on the latest commit timestamp.
+ # e.g. 2020.1.510 is the version built based on a commit that was made
+ # on 2020/01/05 10:00
+ local VER=`git log -1 --format=%cd --date="format:%Y.%-m.%-d%H"`
+ local COMMIT=`git log -1 --format=%H`
+ echo "**** Preparing nightly release : $VER ***"
+
+ # Update package.json
+ (cat package.json | jq --arg VER "${VER}" '
+.version=$VER |
+.preview=true |
+.name="go-nightly" |
+.displayName="Go Nightly" |
+.publisher="golang" |
+.description="Rich Go language support for Visual Studio Code (Nightly)" |
+.author.name="Go Team at Google" |
+.repository.url="https://github.com/golang/vscode-go" |
+.bugs.url="https://github.com/golang/vscode-go/issues"
+') > /tmp/package.json && mv /tmp/package.json package.json
+
+ # Replace CHANGELOG.md with CHANGELOG.md.nightly + Release commit info.
+ printf "**Release ${VER} @ ${COMMIT}** \n\n" | cat - CHANGELOG.md.nightly > /tmp/CHANGELOG.md.new && mv /tmp/CHANGELOG.md.new CHANGELOG.md
+ # Replace the heading of README.md with the heading for Go Nightly.
+ sed '/^# Go for Visual Studio Code$/d' README.md | cat README.md.nightly - > /tmp/README.md.new && mv /tmp/README.md.new README.md
+}
+
main() {
cd "$(root_dir)" # always run from the script root.
case "$1" in
@@ -74,9 +101,12 @@
setup_virtual_display
run_test
;;
+ "prepare_nightly")
+ prepare_nightly
+ ;;
*)
usage
exit 2
esac
}
-main $@
\ No newline at end of file
+main $@
diff --git a/build/cloudbuild.container.yaml b/build/cloudbuild.container.yaml
new file mode 100644
index 0000000..12d8ad7
--- /dev/null
+++ b/build/cloudbuild.container.yaml
@@ -0,0 +1,5 @@
+steps:
+- name: 'gcr.io/cloud-builders/docker'
+ args: ['build', '-t', 'gcr.io/$PROJECT_ID/vscode-test-env', '-f', 'build/Dockerfile', '.']
+images:
+ - 'gcr.io/$PROJECT_ID/vscode-test-env'
diff --git a/build/cloudbuild.yaml b/build/cloudbuild.yaml
new file mode 100644
index 0000000..a678121
--- /dev/null
+++ b/build/cloudbuild.yaml
@@ -0,0 +1,9 @@
+steps:
+- name: 'gcr.io/$PROJECT_ID/vscode-test-env'
+ entrypoint: "./build/all.bash"
+ args: ['ci']
+ env:
+ - 'BUILD=$BUILD_ID'
+ - 'PROJECT=$PROJECT_ID'
+ - 'REV=$REVISION_ID'
+timeout: 600s
diff --git a/build/merge.bash b/build/merge.bash
new file mode 100755
index 0000000..971f10a
--- /dev/null
+++ b/build/merge.bash
@@ -0,0 +1,34 @@
+#! /bin/bash
+set -euo pipefail
+
+# In order to sync with upstream, run merge.bash
+
+# TODO(hyangah): commands for building docker container and running tests locally with docker run.
+root_dir() {
+ local script_name=$(readlink -f "${0}")
+ local script_dir=$(dirname "${script_name}")
+ local parent_dir=$(dirname "${script_dir}")
+ echo "${parent_dir}"
+}
+
+ROOT="$(root_dir)"
+cd "${ROOT}" # always run from the root directory.
+
+WORKTREE="$(mktemp -d)"
+BRANCH="sync/merge-upstream-$(date +%Y%m%d%H%M%S)"
+
+git fetch
+git worktree add --track -b "${BRANCH}" "${WORKTREE}" origin/master
+
+cd "${WORKTREE}"
+export GIT_GOFMT_HOOK=off
+git merge --no-commit "origin/upstream" || echo "Ignoring conflict..."
+
+COMMIT=`git log --format=%h -n 1 "origin/upstream"`
+
+gcloud builds submit --config=build/cloudbuild.yaml || echo "Build failed. Please address the issue..."
+
+git commit -m "sync: merge microsoft/vscode-go@${COMMIT} into master"
+
+git codereview mail HEAD
+cd - && git worktree remove "${WORKTREE}"
diff --git a/package-lock.json b/package-lock.json
index a3fbee8..7640a7b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,6 +1,6 @@
{
- "name": "Go",
- "version": "0.14.2",
+ "name": "go-nightly",
+ "version": "0.0.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -75,6 +75,15 @@
"integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==",
"dev": true
},
+ "@types/adm-zip": {
+ "version": "0.4.33",
+ "resolved": "https://registry.npmjs.org/@types/adm-zip/-/adm-zip-0.4.33.tgz",
+ "integrity": "sha512-WM0DCWFLjXtddl0fu0+iN2ZF+qz8RF9RddG5OSy/S90AQz01Fu8lHn/3oTIZDxvG8gVcnBLAHMHOdBLbV6m6Mw==",
+ "dev": true,
+ "requires": {
+ "@types/node": "*"
+ }
+ },
"@types/deep-equal": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@types/deep-equal/-/deep-equal-1.0.1.tgz",
@@ -155,6 +164,12 @@
"integrity": "sha512-WJZtZlinE3meRdH+I7wTsIhpz/GLhqEQwmPGeh4s1irWLwMzCeTV8WZ+pgPTwrDXoafVUWwo1LiZ9HJVHFlJSQ==",
"dev": true
},
+ "adm-zip": {
+ "version": "0.4.14",
+ "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.4.14.tgz",
+ "integrity": "sha512-/9aQCnQHF+0IiCl0qhXoK7qs//SwYE7zX8lsr/DNk1BRAHYxeLZPL4pguwK29gUEqasYQjqPtEpDRSWEkdHn9g==",
+ "dev": true
+ },
"agent-base": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz",
diff --git a/package.json b/package.json
index 9799f2b..dec77ba 100644
--- a/package.json
+++ b/package.json
@@ -1,11 +1,13 @@
{
- "name": "Go",
- "version": "0.14.2",
- "publisher": "ms-vscode",
- "description": "Rich Go language support for Visual Studio Code",
+ "name": "go-nightly",
+ "displayName": "Go Nightly",
+ "version": "0.0.0",
+ "publisher": "golang",
+ "description": "Rich Go language support for Visual Studio Code (Nightly)",
"author": {
- "name": "Microsoft Corporation - Development Labs"
+ "name": "Go Team at Google"
},
+ "preview": true,
"license": "MIT",
"icon": "images/go-logo-blue.png",
"categories": [
@@ -22,7 +24,10 @@
"private": true,
"repository": {
"type": "git",
- "url": "https://github.com/Microsoft/vscode-go.git"
+ "url": "https://github.com/golang/vscode-go"
+ },
+ "bugs": {
+ "url": "https://github.com/golang/vscode-go/issues"
},
"keywords": [
"multi-root ready"
@@ -40,6 +45,7 @@
},
"extensionDependencies": [],
"dependencies": {
+ "deep-equal": "^2.0.2",
"diff": "^4.0.2",
"json-rpc2": "^1.0.2",
"moment": "^2.24.0",
@@ -49,10 +55,12 @@
"vscode-debugprotocol": "^1.40.0",
"vscode-extension-telemetry": "^0.1.2",
"vscode-languageclient": "6.1.0",
- "web-request": "^1.0.7",
- "deep-equal": "^2.0.2"
+ "web-request": "^1.0.7"
},
"devDependencies": {
+ "adm-zip": "^0.4.14",
+ "@types/adm-zip": "^0.4.33",
+ "@types/deep-equal": "^1.0.1",
"@types/fs-extra": "^8.1.0",
"@types/glob": "^7.1.1",
"@types/mocha": "^7.0.2",
@@ -67,8 +75,7 @@
"sinon": "^9.0.2",
"tslint": "^6.1.1",
"typescript": "^3.8.3",
- "vscode-test": "^1.3.0",
- "@types/deep-equal": "^1.0.1"
+ "vscode-test": "^1.3.0"
},
"engines": {
"vscode": "^1.41.0"
@@ -1127,6 +1134,16 @@
},
"description": "Use this setting to enable/disable experimental features from the language server."
},
+ "go.trace.server": {
+ "type": "string",
+ "enum": [
+ "off",
+ "messages",
+ "verbose"
+ ],
+ "default": "off",
+ "description": "Trace the communication between VS Code and the Go language server."
+ },
"go.useGoProxyToCheckForToolUpdates": {
"type": "boolean",
"default": true,
diff --git a/src/avlTree.ts b/src/avlTree.ts
index 632ad90..56ac9ce 100644
--- a/src/avlTree.ts
+++ b/src/avlTree.ts
@@ -41,7 +41,7 @@
* @param key The key of the new node.
* @param value The value of the new node.
*/
- constructor(public key: K, public value: V) {}
+ constructor(public key: K, public value: V) { }
/**
* Convenience function to get the height of the left child of the node,
diff --git a/src/goCheck.ts b/src/goCheck.ts
index 2cc110a..597e018 100644
--- a/src/goCheck.ts
+++ b/src/goCheck.ts
@@ -8,7 +8,7 @@
import path = require('path');
import vscode = require('vscode');
import { goBuild } from './goBuild';
-import { parseLanguageServerConfig } from './goLanguageServer';
+import { buildLanguageServerConfig } from './goLanguageServer';
import { goLint } from './goLint';
import { buildDiagnosticCollection, lintDiagnosticCollection, vetDiagnosticCollection } from './goMain';
import { isModSupported } from './goModules';
@@ -59,7 +59,7 @@
// If a user has enabled diagnostics via a language server,
// then we disable running build or vet to avoid duplicate errors and warnings.
- const lspConfig = parseLanguageServerConfig();
+ const lspConfig = buildLanguageServerConfig();
const disableBuildAndVet = lspConfig.enabled && lspConfig.features.diagnostics;
let testPromise: Thenable<boolean>;
diff --git a/src/goInstallTools.ts b/src/goInstallTools.ts
index 495913d..2475f32 100644
--- a/src/goInstallTools.ts
+++ b/src/goInstallTools.ts
@@ -11,6 +11,7 @@
import { SemVer } from 'semver';
import vscode = require('vscode');
import { getLanguageServerToolPath } from './goLanguageServer';
+import { restartLanguageServer } from './goMain';
import { envPath, getToolFromToolPath } from './goPath';
import { hideGoStatus, outputChannel, showGoStatus } from './goStatus';
import {
@@ -23,7 +24,8 @@
getTool,
hasModSuffix,
isGocode,
- Tool
+ Tool,
+ ToolAtVersion
} from './goTools';
import {
getBinPath,
@@ -31,6 +33,7 @@
getGoConfig,
getGoVersion,
getTempFilePath,
+ getToolsEnvVars,
getToolsGopath,
GoVersion,
resolvePath
@@ -91,14 +94,6 @@
}
/**
- * ToolAtVersion is a Tool with version annotation.
- * Lack of version implies the latest version
- */
-export interface ToolAtVersion extends Tool {
- version?: SemVer;
-}
-
-/**
* Installs given array of missing tools. If no input is given, the all tools are installed
*
* @param missing array of tool names and optionally, their versions to be installed.
@@ -119,14 +114,12 @@
// http.proxy setting takes precedence over environment variables
const httpProxy = vscode.workspace.getConfiguration('http', null).get('proxy');
- let envForTools = Object.assign({}, process.env);
+ const envForTools = Object.assign({}, process.env, getToolsEnvVars());
if (httpProxy) {
- envForTools = Object.assign({}, process.env, {
- http_proxy: httpProxy,
- HTTP_PROXY: httpProxy,
- https_proxy: httpProxy,
- HTTPS_PROXY: httpProxy
- });
+ envForTools['http_proxy'] = httpProxy;
+ envForTools['HTTP_PROXY'] = httpProxy;
+ envForTools['https_proxy'] = httpProxy;
+ envForTools['HTTPS_PROXY'] = httpProxy;
}
outputChannel.show();
@@ -197,7 +190,7 @@
.reduce((res: Promise<string[]>, tool: ToolAtVersion) => {
return res.then(
(sofar) =>
- new Promise<string[]>((resolve, reject) => {
+ new Promise<string[]>(async (resolve, reject) => {
// Disable modules for tools which are installed with the "..." wildcard.
// TODO: ... will be supported in Go 1.13, so enable these tools to use modules then.
const modulesOffForTool = modulesOff || disableModulesForWildcard(tool, goVersion);
@@ -210,17 +203,18 @@
tmpGoModFile = path.join(toolsTmpDir, 'go.mod');
fs.writeFileSync(tmpGoModFile, 'module tools');
}
+ let importPath: string;
+ if (modulesOffForTool) {
+ importPath = getImportPath(tool, goVersion);
+ } else {
+ importPath = getImportPathWithVersion(tool, tool.version, goVersion);
+ }
- const opts = {
- env: envForTools,
- cwd: toolsTmpDir
- };
const callback = (err: Error, stdout: string, stderr: string) => {
// Make sure to delete the temporary go.mod file, if it exists.
if (tmpGoModFile && fs.existsSync(tmpGoModFile)) {
fs.unlinkSync(tmpGoModFile);
}
- const importPath = getImportPathWithVersion(tool, tool.version, goVersion);
if (err) {
outputChannel.appendLine('Installing ' + importPath + ' FAILED');
const failureReason = tool.name + ';;' + err + stdout.toString() + stderr.toString();
@@ -231,66 +225,51 @@
}
};
- let closeToolPromise = Promise.resolve(true);
- const toolBinPath = getBinPath(tool.name);
- if (path.isAbsolute(toolBinPath) && isGocode(tool)) {
- closeToolPromise = new Promise<boolean>((innerResolve) => {
- cp.execFile(toolBinPath, ['close'], {}, (err, stdout, stderr) => {
- if (stderr && stderr.indexOf(`rpc: can't find service Server.`) > -1) {
- outputChannel.appendLine(
- 'Installing gocode aborted as existing process cannot be closed. Please kill the running process for gocode and try again.'
- );
- return innerResolve(false);
- }
- innerResolve(true);
- });
- });
- }
-
- closeToolPromise.then((success) => {
- if (!success) {
+ // Perform any on-close actions before reinstalling the tool.
+ if (tool.close) {
+ const errMsg = await tool.close();
+ if (errMsg) {
+ outputChannel.appendLine(errMsg);
resolve([...sofar, null]);
return;
}
- const args = ['get', '-v'];
- // Only get tools at master if we are not using modules.
- if (modulesOffForTool) {
- args.push('-u');
- }
- // Tools with a "mod" suffix should not be installed,
- // instead we run "go build -o" to rename them.
- if (hasModSuffix(tool)) {
- args.push('-d');
- }
- let importPath: string;
- if (modulesOffForTool) {
- importPath = getImportPath(tool, goVersion);
+ }
+ const args = ['get', '-v'];
+ // Only get tools at master if we are not using modules.
+ if (modulesOffForTool) {
+ args.push('-u');
+ }
+ // Tools with a "mod" suffix should not be installed,
+ // instead we run "go build -o" to rename them.
+ if (hasModSuffix(tool)) {
+ args.push('-d');
+ }
+ args.push(importPath);
+ const opts = {
+ env: envForTools,
+ cwd: toolsTmpDir
+ };
+ cp.execFile(goRuntimePath, args, opts, (err, stdout, stderr) => {
+ if (stderr.indexOf('unexpected directory layout:') > -1) {
+ outputChannel.appendLine(
+ `Installing ${importPath} failed with error "unexpected directory layout". Retrying...`
+ );
+ cp.execFile(goRuntimePath, args, opts, callback);
+ } else if (!err && hasModSuffix(tool)) {
+ const outputFile = path.join(
+ toolsGopath,
+ 'bin',
+ process.platform === 'win32' ? `${tool.name}.exe` : tool.name
+ );
+ cp.execFile(
+ goRuntimePath,
+ ['build', '-o', outputFile, getImportPath(tool, goVersion)],
+ opts,
+ callback
+ );
} else {
- importPath = getImportPathWithVersion(tool, tool.version, goVersion);
+ callback(err, stdout, stderr);
}
- args.push(importPath);
- cp.execFile(goRuntimePath, args, opts, (err, stdout, stderr) => {
- if (stderr.indexOf('unexpected directory layout:') > -1) {
- outputChannel.appendLine(
- `Installing ${importPath} failed with error "unexpected directory layout". Retrying...`
- );
- cp.execFile(goRuntimePath, args, opts, callback);
- } else if (!err && hasModSuffix(tool)) {
- const outputFile = path.join(
- toolsGopath,
- 'bin',
- process.platform === 'win32' ? `${tool.name}.exe` : tool.name
- );
- cp.execFile(
- goRuntimePath,
- ['build', '-o', outputFile, getImportPath(tool, goVersion)],
- opts,
- callback
- );
- } else {
- callback(err, stdout, stderr);
- }
- });
});
})
);
@@ -299,10 +278,12 @@
outputChannel.appendLine(''); // Blank line for spacing
const failures = res.filter((x) => x != null);
if (failures.length === 0) {
- if (containsString(missing, 'gopls')) {
- outputChannel.appendLine('Reload VS Code window to use the Go language server');
- }
outputChannel.appendLine('All tools successfully installed. You are ready to Go :).');
+
+ // Restart the language server since a new binary has been installed.
+ if (containsString(missing, 'gopls')) {
+ restartLanguageServer();
+ }
return;
}
@@ -324,23 +305,15 @@
}
const goVersion = await getGoVersion();
- // Show error messages for outdated tools.
- if (goVersion.lt('1.9')) {
- let outdatedErrorMsg;
- switch (tool.name) {
- case 'golint':
- outdatedErrorMsg =
- 'golint no longer supports go1.8 or below, update your settings to use golangci-lint as go.lintTool and install golangci-lint';
- break;
- case 'gotests':
- outdatedErrorMsg =
- 'Generate unit tests feature is not supported as gotests tool needs go1.9 or higher.';
- break;
- }
- if (outdatedErrorMsg) {
- vscode.window.showInformationMessage(outdatedErrorMsg);
- return;
- }
+
+ // Show error messages for outdated tools or outdated Go versions.
+ if (tool.minimumGoVersion && goVersion.lt(tool.minimumGoVersion.format())) {
+ vscode.window.showInformationMessage(`You are using go${goVersion.format()}, but ${tool.name} requires at least go${tool.minimumGoVersion.format()}.`);
+ return;
+ }
+ if (tool.maximumGoVersion && goVersion.gt(tool.maximumGoVersion.format())) {
+ vscode.window.showInformationMessage(`You are using go${goVersion.format()}, but ${tool.name} only supports go${tool.maximumGoVersion.format()} and below.`);
+ return;
}
const installOptions = ['Install'];
@@ -389,7 +362,7 @@
choices.push('Release Notes');
}
if (newVersion) {
- updateMsg = `New version of ${tool.name} (${newVersion}) is available. Please update for an improved experience.`;
+ updateMsg = `A new version of ${tool.name} (v${newVersion}) is available. Please update for an improved experience.`;
}
vscode.window.showInformationMessage(updateMsg, ...choices).then((selected) => {
switch (selected) {
@@ -520,9 +493,8 @@
vscode.window.showInformationMessage(promptMsg, installLabel, disableLabel).then((selected) => {
if (selected === installLabel) {
installTools([getTool('gopls')], goVersion).then(() => {
- vscode.window.showInformationMessage(
- 'Reload VS Code window to enable the use of Go language server'
- );
+ // Restart the language server since the binary has changed.
+ restartLanguageServer();
});
} else if (selected === disableLabel) {
const goConfig = getGoConfig();
diff --git a/src/goLanguageServer.ts b/src/goLanguageServer.ts
index 8a5ecdc..be540b8 100644
--- a/src/goLanguageServer.ts
+++ b/src/goLanguageServer.ts
@@ -7,28 +7,41 @@
import cp = require('child_process');
import deepEqual = require('deep-equal');
+import fs = require('fs');
import moment = require('moment');
import path = require('path');
import semver = require('semver');
import util = require('util');
import vscode = require('vscode');
import {
- Command,
- HandleDiagnosticsSignature,
- LanguageClient,
- ProvideCompletionItemsSignature,
- ProvideDocumentLinksSignature,
- RevealOutputChannelOn
+ CloseAction, Command, ErrorAction, HandleDiagnosticsSignature, InitializeError, LanguageClient,
+ Message, ProvideCompletionItemsSignature, ProvideDocumentLinksSignature, RevealOutputChannelOn,
} from 'vscode-languageclient';
import WebRequest = require('web-request');
+import { GoDefinitionProvider } from './goDeclaration';
+import { GoHoverProvider } from './goExtraInfo';
+import { GoDocumentFormattingEditProvider } from './goFormat';
+import { GoImplementationProvider } from './goImplementations';
import { promptForMissingTool, promptForUpdatingTool } from './goInstallTools';
+import { parseLiveFile } from './goLiveErrors';
+import { restartLanguageServer } from './goMain';
+import { GO_MODE } from './goMode';
+import { GoDocumentSymbolProvider } from './goOutline';
import { getToolFromToolPath } from './goPath';
+import { GoReferenceProvider } from './goReferences';
+import { GoRenameProvider } from './goRename';
+import { GoSignatureHelpProvider } from './goSignature';
+import { GoCompletionItemProvider } from './goSuggest';
+import { GoWorkspaceSymbolProvider } from './goSymbol';
import { getTool, Tool } from './goTools';
+import { GoTypeDefinitionProvider } from './goTypeDefinition';
+import { getFromGlobalState, updateGlobalState } from './stateUtils';
import { getBinPath, getCurrentGoPath, getGoConfig, getToolsEnvVars } from './util';
interface LanguageServerConfig {
serverName: string;
path: string;
+ modtime: Date;
enabled: boolean;
flags: string[];
env: any;
@@ -47,38 +60,50 @@
let latestConfig: LanguageServerConfig;
let serverOutputChannel: vscode.OutputChannel;
-// startLanguageServer starts the language server (if enabled), returning
-// true on success.
-export async function registerLanguageFeatures(ctx: vscode.ExtensionContext): Promise<boolean> {
- // Subscribe to notifications for changes to the configuration of the language server.
- ctx.subscriptions.push(vscode.workspace.onDidChangeConfiguration((e) => watchLanguageServerConfiguration(e)));
+// defaultLanguageProviders is the list of providers currently registered.
+let defaultLanguageProviders: vscode.Disposable[] = [];
- const config = parseLanguageServerConfig();
- if (!config.enabled) {
- return false;
- }
+// restartCommand is the command used by the user to restart the language
+// server.
+let restartCommand: vscode.Disposable;
- // Support a command to restart the language server, if it's enabled.
- ctx.subscriptions.push(vscode.commands.registerCommand('go.languageserver.restart', () => {
- return startLanguageServer(ctx, parseLanguageServerConfig());
- }));
+// When enabled, users may be prompted to fill out the gopls survey.
+const goplsSurveyOn: boolean = false;
+
+// startLanguageServerWithFallback starts the language server, if enabled,
+// or falls back to the default language providers.
+export async function startLanguageServerWithFallback(ctx: vscode.ExtensionContext, activation: boolean) {
+ const cfg = buildLanguageServerConfig();
// If the language server is gopls, we can check if the user needs to
- // update their gopls version.
- if (config.serverName === 'gopls') {
- const tool = getTool(config.serverName);
- if (!tool) {
- return false;
- }
- const versionToUpdate = await shouldUpdateLanguageServer(tool, config.path, config.checkForUpdates);
- if (versionToUpdate) {
- promptForUpdatingTool(tool.name, versionToUpdate);
+ // update their gopls version. We do this only once per VS Code
+ // activation to avoid inundating the user.
+ if (activation && cfg.enabled && cfg.serverName === 'gopls') {
+ const tool = getTool(cfg.serverName);
+ if (tool) {
+ const versionToUpdate = await shouldUpdateLanguageServer(tool, cfg.path, cfg.checkForUpdates);
+ if (versionToUpdate) {
+ promptForUpdatingTool(tool.name, versionToUpdate);
+ } else if (goplsSurveyOn) {
+ // Only prompt users to fill out the gopls survey if we are not
+ // also prompting them to update (both would be too much).
+ const timeout = 1000 * 60 * 60; // 1 hour
+ setTimeout(async () => {
+ const surveyCfg = await maybePromptForGoplsSurvey();
+ flushSurveyConfig(surveyCfg);
+ }, timeout);
+ }
}
}
- // This function handles the case when the server isn't started yet,
- // so we can call it to start the language server.
- return startLanguageServer(ctx, config);
+ const started = await startLanguageServer(ctx, cfg);
+
+ // If the server has been disabled, or failed to start,
+ // fall back to the default providers, while making sure not to
+ // re-register any providers.
+ if (!started && defaultLanguageProviders.length === 0) {
+ registerDefaultProviders(ctx);
+ }
}
async function startLanguageServer(ctx: vscode.ExtensionContext, config: LanguageServerConfig): Promise<boolean> {
@@ -97,29 +122,47 @@
// Check if we should recreate the language client. This may be necessary
// if the user has changed settings in their config.
if (!deepEqual(latestConfig, config)) {
- // Track the latest config used to start the language server.
+ // Track the latest config used to start the language server,
+ // and rebuild the language client.
latestConfig = config;
-
- // If the user has not enabled or installed the language server, return.
- if (!config.enabled || !config.path) {
- return false;
- }
- buildLanguageClient(config);
+ languageClient = await buildLanguageClient(config);
}
+ // If the user has not enabled the language server, return early.
+ if (!config.enabled) {
+ return false;
+ }
+
+ // Set up the command to allow the user to manually restart the
+ // language server.
+ if (!restartCommand) {
+ restartCommand = vscode.commands.registerCommand('go.languageserver.restart', async () => {
+ // TODO(rstambler): Enable this behavior when gopls reaches v1.0.
+ if (false) {
+ await suggestGoplsIssueReport(`Looks like you're about to manually restart the language server.`);
+ }
+ restartLanguageServer();
+ });
+ ctx.subscriptions.push(restartCommand);
+ }
+
+ // Before starting the language server, make sure to deregister any
+ // currently registered language providers.
+ disposeDefaultProviders();
+
languageServerDisposable = languageClient.start();
ctx.subscriptions.push(languageServerDisposable);
-
return true;
}
-function buildLanguageClient(config: LanguageServerConfig) {
+async function buildLanguageClient(config: LanguageServerConfig): Promise<LanguageClient> {
// Reuse the same output channel for each instance of the server.
- if (!serverOutputChannel) {
+ if (config.enabled && !serverOutputChannel) {
serverOutputChannel = vscode.window.createOutputChannel(config.serverName);
}
- languageClient = new LanguageClient(
- config.serverName,
+ const c = new LanguageClient(
+ 'go', // id
+ config.serverName, // name
{
command: config.path,
args: ['-mode=stdio', ...config.flags],
@@ -136,6 +179,31 @@
},
outputChannel: serverOutputChannel,
revealOutputChannelOn: RevealOutputChannelOn.Never,
+ initializationFailedHandler: (error: WebRequest.ResponseError<InitializeError>): boolean => {
+ vscode.window.showErrorMessage(
+ `The language server is not able to serve any features. Initialization failed: ${error}. `
+ );
+ serverOutputChannel.show();
+ suggestGoplsIssueReport(`The gopls server failed to initialize.`);
+ return false;
+ },
+ errorHandler: {
+ error: (error: Error, message: Message, count: number): ErrorAction => {
+ vscode.window.showErrorMessage(
+ `Error communicating with the language server: ${error}: ${message}.`
+ );
+ // Stick with the default number of 5 crashes before shutdown.
+ if (count >= 5) {
+ return ErrorAction.Shutdown;
+ }
+ return ErrorAction.Continue;
+ },
+ closed: (): CloseAction => {
+ serverOutputChannel.show();
+ suggestGoplsIssueReport(`The connection to gopls has been closed. The gopls server may have crashed.`);
+ return CloseAction.DoNotRestart;
+ },
+ },
middleware: {
handleDiagnostics: (
uri: vscode.Uri,
@@ -212,60 +280,70 @@
}
}
);
- languageClient.onReady().then(() => {
- const capabilities = languageClient.initializeResult && languageClient.initializeResult.capabilities;
- if (!capabilities) {
- return vscode.window.showErrorMessage(
- 'The language server is not able to serve any features at the moment.'
- );
- }
- });
+ return c;
}
-function watchLanguageServerConfiguration(e: vscode.ConfigurationChangeEvent) {
+// registerUsualProviders registers the language feature providers if the language server is not enabled.
+function registerDefaultProviders(ctx: vscode.ExtensionContext) {
+ const completionProvider = new GoCompletionItemProvider(ctx.globalState);
+ defaultLanguageProviders.push(completionProvider);
+ defaultLanguageProviders.push(vscode.languages.registerCompletionItemProvider(GO_MODE, completionProvider, '.', '"'));
+ defaultLanguageProviders.push(vscode.languages.registerHoverProvider(GO_MODE, new GoHoverProvider()));
+ defaultLanguageProviders.push(vscode.languages.registerDefinitionProvider(GO_MODE, new GoDefinitionProvider()));
+ defaultLanguageProviders.push(vscode.languages.registerReferenceProvider(GO_MODE, new GoReferenceProvider()));
+ defaultLanguageProviders.push(
+ vscode.languages.registerDocumentSymbolProvider(GO_MODE, new GoDocumentSymbolProvider())
+ );
+ defaultLanguageProviders.push(vscode.languages.registerWorkspaceSymbolProvider(new GoWorkspaceSymbolProvider()));
+ defaultLanguageProviders.push(
+ vscode.languages.registerSignatureHelpProvider(GO_MODE, new GoSignatureHelpProvider(), '(', ',')
+ );
+ defaultLanguageProviders.push(
+ vscode.languages.registerImplementationProvider(GO_MODE, new GoImplementationProvider())
+ );
+ defaultLanguageProviders.push(
+ vscode.languages.registerDocumentFormattingEditProvider(GO_MODE, new GoDocumentFormattingEditProvider())
+ );
+ defaultLanguageProviders.push(
+ vscode.languages.registerTypeDefinitionProvider(GO_MODE, new GoTypeDefinitionProvider())
+ );
+ defaultLanguageProviders.push(vscode.languages.registerRenameProvider(GO_MODE, new GoRenameProvider()));
+ defaultLanguageProviders.push(vscode.workspace.onDidChangeTextDocument(parseLiveFile, null, ctx.subscriptions));
+
+ for (const provider of defaultLanguageProviders) {
+ ctx.subscriptions.push(provider);
+ }
+}
+
+function disposeDefaultProviders() {
+ for (const disposable of defaultLanguageProviders) {
+ disposable.dispose();
+ }
+ defaultLanguageProviders = [];
+}
+
+export function watchLanguageServerConfiguration(e: vscode.ConfigurationChangeEvent) {
if (!e.affectsConfiguration('go')) {
return;
}
- const config = parseLanguageServerConfig();
- let reloadMessage: string;
-
- // If the user has disabled or enabled the language server.
- if (e.affectsConfiguration('go.useLanguageServer')) {
- if (config.enabled) {
- reloadMessage = 'Reload VS Code window to enable the use of language server';
- } else {
- reloadMessage = 'Reload VS Code window to disable the use of language server';
- }
- }
-
if (
+ e.affectsConfiguration('go.useLanguageServer') ||
e.affectsConfiguration('go.languageServerFlags') ||
e.affectsConfiguration('go.languageServerExperimentalFeatures')
) {
- reloadMessage = 'Reload VS Code window for the changes in language server settings to take effect';
- }
-
- // If there was a change in the configuration of the language server,
- // then ask the user to reload VS Code.
- if (reloadMessage) {
- vscode.window.showInformationMessage(reloadMessage, 'Reload').then((selected) => {
- if (selected === 'Reload') {
- vscode.commands.executeCommand('workbench.action.reloadWindow');
- }
- });
+ restartLanguageServer();
}
}
-export function parseLanguageServerConfig(): LanguageServerConfig {
+export function buildLanguageServerConfig(): LanguageServerConfig {
const goConfig = getGoConfig();
const toolsEnv = getToolsEnvVars();
- const languageServerPath = getLanguageServerToolPath();
- const languageServerName = getToolFromToolPath(languageServerPath);
- return {
- serverName: languageServerName,
- path: languageServerPath,
- enabled: goConfig['useLanguageServer'],
+ const cfg: LanguageServerConfig = {
+ serverName: '',
+ path: '',
+ modtime: null,
+ enabled: goConfig['useLanguageServer'] === true,
flags: goConfig['languageServerFlags'] || [],
features: {
// TODO: We should have configs that match these names.
@@ -274,23 +352,47 @@
documentLink: goConfig['languageServerExperimentalFeatures']['documentLink']
},
env: toolsEnv,
- checkForUpdates: goConfig['useGoProxyToCheckForToolUpdates']
+ checkForUpdates: goConfig['useGoProxyToCheckForToolUpdates'],
};
+ // Don't look for the path if the server is not enabled.
+ if (!cfg.enabled) {
+ return cfg;
+ }
+ const languageServerPath = getLanguageServerToolPath();
+ if (!languageServerPath) {
+ // Assume the getLanguageServerToolPath will show the relevant
+ // errors to the user. Disable the language server.
+ cfg.enabled = false;
+ return cfg;
+ }
+ cfg.path = languageServerPath;
+ cfg.serverName = getToolFromToolPath(cfg.path);
+
+ // Get the mtime of the language server binary so that we always pick up
+ // the right version.
+ const stats = fs.statSync(languageServerPath);
+ if (!stats) {
+ vscode.window.showErrorMessage(`Unable to stat path to language server binary: ${languageServerPath}.
+Please try reinstalling it.`);
+ // Disable the language server.
+ cfg.enabled = false;
+ return cfg;
+ }
+ cfg.modtime = stats.mtime;
+
+ return cfg;
}
/**
*
- * If the user has enabled the language server, return the absolute path to the
- * correct binary. If the required tool is not available, prompt the user to
- * install it. Only gopls is officially supported.
+ * Return the absolute path to the correct binary. If the required tool is not available,
+ * prompt the user to install it. Only gopls is officially supported.
*/
export function getLanguageServerToolPath(): string {
- // If language server is not enabled, return
const goConfig = getGoConfig();
if (!goConfig['useLanguageServer']) {
return;
}
-
// Check that all workspace folders are configured with the same GOPATH.
if (!allFoldersHaveSameGopath()) {
vscode.window.showInformationMessage(
@@ -335,10 +437,8 @@
return vscode.workspace.workspaceFolders.find((x) => tempGopath !== getCurrentGoPath(x.uri)) ? false : true;
}
-const acceptGoplsPrerelease = false;
-const defaultLatestVersion = semver.coerce('0.4.0');
-const defaultLatestVersionTime = moment('2020-04-08', 'YYYY-MM-DD');
-async function shouldUpdateLanguageServer(
+const acceptGoplsPrerelease = true; // For nightly, we accept the prerelease version.
+export async function shouldUpdateLanguageServer(
tool: Tool,
languageServerToolPath: string,
makeProxyCall: boolean
@@ -349,19 +449,19 @@
}
// First, run the "gopls version" command and parse its results.
- const usersVersion = await goplsVersion(languageServerToolPath);
+ const usersVersion = await getLocalGoplsVersion(languageServerToolPath);
// We might have a developer version. Don't make the user update.
if (usersVersion === '(devel)') {
return null;
}
- // Get the latest gopls version.
- let latestVersion = makeProxyCall ? await latestGopls(tool) : defaultLatestVersion;
+ // Get the latest gopls version. If it is for nightly, using the prereleased version is ok.
+ let latestVersion = makeProxyCall ? await getLatestGoplsVersion(tool) : tool.latestVersion;
// If we failed to get the gopls version, pick the one we know to be latest at the time of this extension's last update
if (!latestVersion) {
- latestVersion = defaultLatestVersion;
+ latestVersion = tool.latestVersion;
}
// If "gopls" is so old that it doesn't have the "gopls version" command,
@@ -373,12 +473,12 @@
// The user may have downloaded golang.org/x/tools/gopls@master,
// which means that they have a pseudoversion.
- const usersTime = parsePseudoversionTimestamp(usersVersion);
+ const usersTime = parseTimestampFromPseudoversion(usersVersion);
// If the user has a pseudoversion, get the timestamp for the latest gopls version and compare.
if (usersTime) {
- let latestTime = makeProxyCall ? await goplsVersionTimestamp(tool, latestVersion) : defaultLatestVersionTime;
+ let latestTime = makeProxyCall ? await getTimestampForVersion(tool, latestVersion) : tool.latestVersionTimestamp;
if (!latestTime) {
- latestTime = defaultLatestVersionTime;
+ latestTime = tool.latestVersionTimestamp;
}
return usersTime.isBefore(latestTime) ? latestVersion : null;
}
@@ -388,13 +488,13 @@
return semver.lt(usersVersion, latestVersion) ? latestVersion : null;
}
-// Copied from src/cmd/go/internal/modfetch.
+// Copied from src/cmd/go/internal/modfetch.go.
const pseudoVersionRE = /^v[0-9]+\.(0\.0-|\d+\.\d+-([^+]*\.)?0\.)\d{14}-[A-Za-z0-9]+(\+incompatible)?$/;
-// parsePseudoVersion reports whether v is a pseudo-version.
-// The timestamp is the center component, and it has the format "YYYYMMDDHHmmss".
-
-function parsePseudoversionTimestamp(version: string): moment.Moment {
+// parseTimestampFromPseudoversion returns the timestamp for the given
+// pseudoversion. The timestamp is the center component, and it has the
+// format "YYYYMMDDHHmmss".
+function parseTimestampFromPseudoversion(version: string): moment.Moment {
const split = version.split('-');
if (split.length < 2) {
return null;
@@ -430,7 +530,7 @@
return moment.utc(timestamp, 'YYYYMMDDHHmmss');
}
-async function goplsVersionTimestamp(tool: Tool, version: semver.SemVer): Promise<moment.Moment> {
+export async function getTimestampForVersion(tool: Tool, version: semver.SemVer): Promise<moment.Moment> {
const data = await goProxyRequest(tool, `v${version.format()}.info`);
if (!data) {
return null;
@@ -439,7 +539,7 @@
return time;
}
-async function latestGopls(tool: Tool): Promise<semver.SemVer> {
+export async function getLatestGoplsVersion(tool: Tool): Promise<semver.SemVer> {
// If the user has a version of gopls that we understand,
// ask the proxy for the latest version, and if the user's version is older,
// prompt them to update.
@@ -470,7 +570,10 @@
return versions.find((version) => !version.prerelease || !version.prerelease.length);
}
-async function goplsVersion(goplsPath: string): Promise<string> {
+// getLocalGoplsVersion returns the version of gopls that is currently
+// installed on the user's machine. This is determined by running the
+// `gopls version` command.
+export async function getLocalGoplsVersion(goplsPath: string): Promise<string> {
const env = getToolsEnvVars();
const execFile = util.promisify(cp.execFile);
let output: any;
@@ -527,9 +630,15 @@
}
async function goProxyRequest(tool: Tool, endpoint: string): Promise<any> {
- const proxies = goProxy();
+ // Get the user's value of GOPROXY.
+ // If it is not set, we cannot make the request.
+ const output: string = process.env['GOPROXY'];
+ if (!output || !output.trim()) {
+ return null;
+ }
// Try each URL set in the user's GOPROXY environment variable.
// If none is set, don't make the request.
+ const proxies = output.trim().split(',|');
for (const proxy of proxies) {
if (proxy === 'direct') {
continue;
@@ -548,11 +657,184 @@
return null;
}
-function goProxy(): string[] {
- const output: string = process.env['GOPROXY'];
- if (!output || !output.trim()) {
- return [];
+// SurveyConfig is the set of global properties used to determine if
+// we should prompt a user to take the gopls survey.
+export interface SurveyConfig {
+ // prompt is true if the user can be prompted to take the survey.
+ // It is false if the user has responded "Never" to the prompt.
+ prompt?: boolean;
+
+ // promptThisMonth is true if we have used a random number generator
+ // to determine if the user should be prompted this month.
+ // It is undefined if we have not yet made the determination.
+ promptThisMonth?: boolean;
+
+ // promptThisMonthTimestamp is the date on which we determined if the user
+ // should be prompted this month.
+ promptThisMonthTimestamp?: Date;
+
+ // lastDatePrompted is the most recent date that the user has been prompted.
+ lastDatePrompted?: Date;
+
+ // lastDateAccepted is the most recent date that the user responded "Yes"
+ // to the survey prompt. The user need not have completed the survey.
+ lastDateAccepted?: Date;
+}
+
+async function maybePromptForGoplsSurvey(): Promise<SurveyConfig> {
+ const now = new Date();
+ const cfg = getSurveyConfig();
+ const prompt = shouldPromptForGoplsSurvey(now, cfg);
+ if (!prompt) {
+ return cfg;
}
- const split = output.trim().split(',');
- return split;
+ const selected = await vscode.window.showInformationMessage(`Looks like you're using gopls, the Go language server.
+Would you be willing to fill out a quick survey about your experience with gopls?`, 'Yes', 'Not now', 'Never');
+
+ // Update the time last asked.
+ cfg.lastDatePrompted = now;
+
+ switch (selected) {
+ case 'Yes':
+ cfg.lastDateAccepted = now;
+ cfg.prompt = true;
+
+ // Open the link to the survey.
+ vscode.env.openExternal(vscode.Uri.parse('https://www.whattimeisitrightnow.com/'));
+ break;
+ case 'Not now':
+ cfg.prompt = true;
+
+ vscode.window.showInformationMessage(`No problem! We'll ask you again another time.`);
+ break;
+ case 'Never':
+ cfg.prompt = false;
+
+ vscode.window.showInformationMessage(`No problem! We won't ask again.`);
+ break;
+ }
+ return cfg;
+}
+
+export function shouldPromptForGoplsSurvey(now: Date, cfg: SurveyConfig): boolean {
+ // If the prompt value is not set, assume we haven't prompted the user
+ // and should do so.
+ if (cfg.prompt === undefined) {
+ cfg.prompt = true;
+ }
+ if (!cfg.prompt) {
+ return false;
+ }
+
+ // Check if the user has taken the survey in the last year.
+ // Don't prompt them if they have been.
+ if (cfg.lastDateAccepted) {
+ if (daysBetween(now, cfg.lastDateAccepted) < 365) {
+ return false;
+ }
+ }
+
+ // Check if the user has been prompted for the survey in the last 90 days.
+ // Don't prompt them if they have been.
+ if (cfg.lastDatePrompted) {
+ if (daysBetween(now, cfg.lastDatePrompted) < 90) {
+ return false;
+ }
+ }
+
+ // Check if the extension has been activated this month.
+ if (cfg.promptThisMonthTimestamp) {
+ // The extension has been activated this month, so we should have already
+ // decided if the user should be prompted.
+ if (daysBetween(now, cfg.promptThisMonthTimestamp) < 30) {
+ return cfg.promptThisMonth;
+ }
+ }
+ // This is the first activation this month (or ever), so decide if we
+ // should prompt the user. This is done by generating a random number
+ // and % 20 to get a 5% chance.
+ const r = Math.floor(Math.random() * 20);
+ cfg.promptThisMonth = (r % 20 === 0);
+ cfg.promptThisMonthTimestamp = now;
+
+ return cfg.promptThisMonth;
+}
+
+export const goplsSurveyConfig = 'goplsSurveyConfig';
+
+function getSurveyConfig(): SurveyConfig {
+ const saved = getFromGlobalState(goplsSurveyConfig);
+ if (saved === undefined) {
+ return {};
+ }
+ try {
+ const cfg = JSON.parse(saved, (key: string, value: any) => {
+ // Make sure values that should be dates are correctly converted.
+ if (key.includes('Date')) {
+ return new Date(value);
+ }
+ return value;
+ });
+ return cfg;
+ } catch (err) {
+ console.log(`Error parsing JSON from ${saved}: ${err}`);
+ return {};
+ }
+}
+
+function flushSurveyConfig(cfg: SurveyConfig) {
+ updateGlobalState(goplsSurveyConfig, JSON.stringify(cfg));
+}
+
+// suggestGoplsIssueReport prompts users to file an issue with gopls.
+async function suggestGoplsIssueReport(msg: string) {
+ if (latestConfig.serverName !== 'gopls') {
+ return;
+ }
+ const promptForIssueOnGoplsRestartKey = `promptForIssueOnGoplsRestart`;
+ let saved: any;
+ try {
+ saved = JSON.parse(getFromGlobalState(promptForIssueOnGoplsRestartKey, true));
+ } catch (err) {
+ console.log(`Failed to parse as JSON ${getFromGlobalState(promptForIssueOnGoplsRestartKey, true)}: ${err}`);
+ return;
+ }
+ // If the user has already seen this prompt, they may have opted-out for
+ // the future. Only prompt again if it's been more than a year since.
+ if (saved['date'] && saved['prompt']) {
+ const dateSaved = new Date(saved['date']);
+ const prompt = <boolean>saved['prompt'];
+ if (!prompt && daysBetween(new Date(), dateSaved) <= 365) {
+ return;
+ }
+ }
+ const selected = await vscode.window.showInformationMessage(`${msg} Would you like to report a gopls issue ? `, 'Yes', 'Next time', 'Never');
+ switch (selected) {
+ case 'Yes':
+ // Run the `gopls bug` command directly for now. When
+ // https://github.com/golang/go/issues/38942 is
+ // resolved, we'll be able to do this through the
+ // language client.
+
+ // Wait for the command to finish before restarting the
+ // server, but don't bother handling errors.
+ const execFile = util.promisify(cp.execFile);
+ await execFile(latestConfig.path, ['bug'], { env: getToolsEnvVars() });
+ break;
+ case 'Next time':
+ break;
+ case 'Never':
+ updateGlobalState(promptForIssueOnGoplsRestartKey, JSON.stringify({
+ prompt: false,
+ date: new Date(),
+ }));
+ break;
+ }
+}
+
+// daysBetween returns the number of days between a and b,
+// assuming that a occurs after b.
+function daysBetween(a: Date, b: Date) {
+ const ms = a.getTime() - b.getTime();
+ return ms / (1000 * 60 * 60 * 24);
}
diff --git a/src/goMain.ts b/src/goMain.ts
index 7fda993..2d71c75 100644
--- a/src/goMain.ts
+++ b/src/goMain.ts
@@ -5,7 +5,6 @@
'use strict';
-import fs = require('fs');
import * as path from 'path';
import vscode = require('vscode');
import { browsePackages } from './goBrowsePackage';
@@ -13,82 +12,54 @@
import { check, notifyIfGeneratedFile, removeTestStatus } from './goCheck';
import { GoCodeActionProvider } from './goCodeAction';
import {
- applyCodeCoverage,
- applyCodeCoverageToAllEditors,
- initCoverageDecorators,
- removeCodeCoverageOnFileSave,
- toggleCoverageCurrentPackage,
- trackCodeCoverageRemovalOnFileChange,
- updateCodeCoverageDecorators
+ applyCodeCoverage, applyCodeCoverageToAllEditors, initCoverageDecorators, removeCodeCoverageOnFileSave,
+ toggleCoverageCurrentPackage, trackCodeCoverageRemovalOnFileChange, updateCodeCoverageDecorators
} from './goCover';
import { GoDebugConfigurationProvider } from './goDebugConfiguration';
-import { GoDefinitionProvider } from './goDeclaration';
import { extractFunction, extractVariable } from './goDoctor';
-import { GoHoverProvider } from './goExtraInfo';
import { runFillStruct } from './goFillStruct';
-import { GoDocumentFormattingEditProvider } from './goFormat';
import * as goGenerateTests from './goGenerateTests';
import { goGetPackage } from './goGetPackage';
import { implCursor } from './goImpl';
-import { GoImplementationProvider } from './goImplementations';
import { addImport, addImportToWorkspace } from './goImport';
import { installCurrentPackage } from './goInstall';
import {
- installAllTools,
- installTools,
- offerToInstallTools,
- promptForMissingTool,
+ installAllTools, installTools, offerToInstallTools, promptForMissingTool,
updateGoPathGoRootFromConfig
} from './goInstallTools';
-import { registerLanguageFeatures } from './goLanguageServer';
+import { startLanguageServerWithFallback, watchLanguageServerConfiguration } from './goLanguageServer';
import { lintCode } from './goLint';
-import { parseLiveFile } from './goLiveErrors';
import { GO_MODE } from './goMode';
import { addTags, removeTags } from './goModifytags';
import { GO111MODULE, isModSupported } from './goModules';
-import { GoDocumentSymbolProvider } from './goOutline';
import { clearCacheForTools, fileExists } from './goPath';
import { playgroundCommand } from './goPlayground';
-import { GoReferenceProvider } from './goReferences';
import { GoReferencesCodeLensProvider } from './goReferencesCodelens';
-import { GoRenameProvider } from './goRename';
import { GoRunTestCodeLensProvider } from './goRunTestCodelens';
-import { GoSignatureHelpProvider } from './goSignature';
import { outputChannel, showHideStatus } from './goStatus';
-import { GoCompletionItemProvider } from './goSuggest';
-import { GoWorkspaceSymbolProvider } from './goSymbol';
import { testAtCursor, testCurrentFile, testCurrentPackage, testPrevious, testWorkspace } from './goTest';
import { getConfiguredTools } from './goTools';
-import { GoTypeDefinitionProvider } from './goTypeDefinition';
import { vetCode } from './goVet';
import {
- getFromGlobalState,
- getFromWorkspaceState,
- setGlobalState,
- setWorkspaceState,
- updateGlobalState,
+ getFromGlobalState, getFromWorkspaceState, setGlobalState, setWorkspaceState, updateGlobalState,
updateWorkspaceState
} from './stateUtils';
import { disposeTelemetryReporter, sendTelemetryEventForConfig } from './telemetry';
import { cancelRunningTests, showTestOutput } from './testUtils';
import {
- cleanupTempDir,
- getBinPath,
- getCurrentGoPath,
- getExtensionCommands,
- getGoConfig,
- getGoVersion,
- getToolsEnvVars,
- getToolsGopath,
- getWorkspaceFolderPath,
- handleDiagnosticErrors,
- isGoPathSet
+ cleanupTempDir, getBinPath, getCurrentGoPath, getExtensionCommands, getGoConfig,
+ getGoVersion, getToolsEnvVars, getToolsGopath, getWorkspaceFolderPath, handleDiagnosticErrors, isGoPathSet
} from './util';
export let buildDiagnosticCollection: vscode.DiagnosticCollection;
export let lintDiagnosticCollection: vscode.DiagnosticCollection;
export let vetDiagnosticCollection: vscode.DiagnosticCollection;
+// restartLanguageServer wraps all of the logic needed to restart the
+// language server. It can be used to enable, disable, or otherwise change
+// the configuration of the server.
+export let restartLanguageServer = () => { return; };
+
export function activate(ctx: vscode.ExtensionContext): void {
setGlobalState(ctx.globalState);
setWorkspaceState(ctx.workspaceState);
@@ -145,12 +116,21 @@
offerToInstallTools();
- // This handles all of the configurations and registrations for the language server.
- // It also registers the necessary language feature providers that the language server may not support.
- const ok = await registerLanguageFeatures(ctx);
- if (!ok) {
- registerUsualProviders(ctx);
- }
+ // Subscribe to notifications for changes to the configuration
+ // of the language server, even if it's not currently in use.
+ ctx.subscriptions.push(vscode.workspace.onDidChangeConfiguration(
+ (e) => watchLanguageServerConfiguration(e)
+ ));
+
+ // Set the function that is used to restart the language server.
+ // This is necessary, even if the language server is not currently
+ // in use.
+ restartLanguageServer = async () => {
+ startLanguageServerWithFallback(ctx, false);
+ };
+
+ // Start the language server, or fallback to the default language providers.
+ startLanguageServerWithFallback(ctx, true);
if (
vscode.window.activeTextEditor &&
@@ -623,28 +603,6 @@
);
}
-// registerUsualProviders registers the language feature providers if the language server is not enabled.
-function registerUsualProviders(ctx: vscode.ExtensionContext) {
- const provider = new GoCompletionItemProvider(ctx.globalState);
- ctx.subscriptions.push(provider);
- ctx.subscriptions.push(vscode.languages.registerCompletionItemProvider(GO_MODE, provider, '.', '"'));
- ctx.subscriptions.push(vscode.languages.registerHoverProvider(GO_MODE, new GoHoverProvider()));
- ctx.subscriptions.push(vscode.languages.registerDefinitionProvider(GO_MODE, new GoDefinitionProvider()));
- ctx.subscriptions.push(vscode.languages.registerReferenceProvider(GO_MODE, new GoReferenceProvider()));
- ctx.subscriptions.push(vscode.languages.registerDocumentSymbolProvider(GO_MODE, new GoDocumentSymbolProvider()));
- ctx.subscriptions.push(vscode.languages.registerWorkspaceSymbolProvider(new GoWorkspaceSymbolProvider()));
- ctx.subscriptions.push(
- vscode.languages.registerSignatureHelpProvider(GO_MODE, new GoSignatureHelpProvider(), '(', ',')
- );
- ctx.subscriptions.push(vscode.languages.registerImplementationProvider(GO_MODE, new GoImplementationProvider()));
- ctx.subscriptions.push(
- vscode.languages.registerDocumentFormattingEditProvider(GO_MODE, new GoDocumentFormattingEditProvider())
- );
- ctx.subscriptions.push(vscode.languages.registerTypeDefinitionProvider(GO_MODE, new GoTypeDefinitionProvider()));
- ctx.subscriptions.push(vscode.languages.registerRenameProvider(GO_MODE, new GoRenameProvider()));
- vscode.workspace.onDidChangeTextDocument(parseLiveFile, null, ctx.subscriptions);
-}
-
function addOnChangeTextDocumentListeners(ctx: vscode.ExtensionContext) {
vscode.workspace.onDidChangeTextDocument(trackCodeCoverageRemovalOnFileChange, null, ctx.subscriptions);
vscode.workspace.onDidChangeTextDocument(removeTestStatus, null, ctx.subscriptions);
diff --git a/src/goModules.ts b/src/goModules.ts
index 35caeb7..6ec5017 100644
--- a/src/goModules.ts
+++ b/src/goModules.ts
@@ -7,6 +7,7 @@
import path = require('path');
import vscode = require('vscode');
import { installTools } from './goInstallTools';
+import { restartLanguageServer } from './goMain';
import { envPath, fixDriveCasingInWindows } from './goPath';
import { getTool } from './goTools';
import { getFromGlobalState, updateGlobalState } from './stateUtils';
@@ -135,12 +136,7 @@
if (goConfig.inspect('useLanguageServer').workspaceFolderValue === false) {
goConfig.update('useLanguageServer', true, vscode.ConfigurationTarget.WorkspaceFolder);
}
- const reloadMsg = 'Reload VS Code window to enable the use of Go language server';
- vscode.window.showInformationMessage(reloadMsg, 'Reload').then((selectedForReload) => {
- if (selectedForReload === 'Reload') {
- vscode.commands.executeCommand('workbench.action.reloadWindow');
- }
- });
+ restartLanguageServer();
}
});
}
diff --git a/src/goTools.ts b/src/goTools.ts
index 7ef0a9b..2a9bb8d 100644
--- a/src/goTools.ts
+++ b/src/goTools.ts
@@ -5,15 +5,45 @@
'use strict';
-import { SemVer } from 'semver';
+import cp = require('child_process');
+import moment = require('moment');
+import path = require('path');
+import semver = require('semver');
+import util = require('util');
import { goLiveErrorsEnabled } from './goLiveErrors';
-import { getGoConfig, GoVersion } from './util';
+import { getBinPath, getGoConfig, GoVersion } from './util';
export interface Tool {
name: string;
importPath: string;
isImportant: boolean;
description: string;
+
+ // latestVersion and latestVersionTimestamp are hardcoded default values
+ // for the last known version of the given tool. We also hardcode values
+ // for the latest known pre-release of the tool for the Nightly extension.
+ latestVersion?: semver.SemVer;
+ latestVersionTimestamp?: moment.Moment;
+ latestPrereleaseVersion?: semver.SemVer;
+ latestPrereleaseVersionTimestamp?: moment.Moment;
+
+ // minimumGoVersion and maximumGoVersion set the range for the versions of
+ // Go with which this tool can be used.
+ minimumGoVersion?: semver.SemVer;
+ maximumGoVersion?: semver.SemVer;
+
+ // close performs any shutdown tasks that a tool must execute before a new
+ // version is installed. It returns a string containing an error message on
+ // failure.
+ close?: () => Promise<string>;
+}
+
+/**
+ * ToolAtVersion is a Tool at a specific version.
+ * Lack of version implies the latest version.
+ */
+export interface ToolAtVersion extends Tool {
+ version?: semver.SemVer;
}
/**
@@ -29,7 +59,7 @@
return tool.importPath;
}
-export function getImportPathWithVersion(tool: Tool, version: SemVer, goVersion: GoVersion): string {
+export function getImportPathWithVersion(tool: Tool, version: semver.SemVer, goVersion: GoVersion): string {
const importPath = getImportPath(tool, goVersion);
if (version) {
return importPath + '@v' + version;
@@ -62,6 +92,10 @@
return allToolsInformation[name];
}
+export function getToolAtVersion(name: string, version?: semver.SemVer): ToolAtVersion {
+ return { ...allToolsInformation[name], version };
+}
+
// hasModSuffix returns true if the given tool has a different, module-specific
// name to avoid conflicts.
export function hasModSuffix(tool: Tool): boolean {
@@ -146,13 +180,30 @@
name: 'gocode',
importPath: 'github.com/mdempsky/gocode',
isImportant: true,
- description: 'Auto-completion, does not work with modules'
+ description: 'Auto-completion, does not work with modules',
+ close: async (): Promise<string> => {
+ const toolBinPath = getBinPath('gocode');
+ if (!path.isAbsolute(toolBinPath)) {
+ return '';
+ }
+ try {
+ const execFile = util.promisify(cp.execFile);
+ const { stderr } = await execFile(toolBinPath, ['close']);
+ if (stderr.indexOf(`rpc: can't find service Server.`) > -1) {
+ return `Installing gocode aborted as existing process cannot be closed. Please kill the running process for gocode and try again.`;
+ }
+ } catch (err) {
+ return `Failed to close gocode process: ${err}.`;
+ }
+ return '';
+ },
},
'gocode-gomod': {
name: 'gocode-gomod',
importPath: 'github.com/stamblerre/gocode',
isImportant: true,
- description: 'Auto-completion, works with modules'
+ description: 'Auto-completion, works with modules',
+ minimumGoVersion: semver.coerce('1.11'),
},
'gopkgs': {
name: 'gopkgs',
@@ -242,13 +293,15 @@
name: 'golint',
importPath: 'golang.org/x/lint/golint',
isImportant: true,
- description: 'Linter'
+ description: 'Linter',
+ minimumGoVersion: semver.coerce('1.9'),
},
'gotests': {
name: 'gotests',
importPath: 'github.com/cweill/gotests/...',
isImportant: false,
- description: 'Generate unit tests'
+ description: 'Generate unit tests',
+ minimumGoVersion: semver.coerce('1.9'),
},
'staticcheck': {
name: 'staticcheck',
@@ -272,7 +325,12 @@
name: 'gopls',
importPath: 'golang.org/x/tools/gopls',
isImportant: false,
- description: 'Language Server from Google'
+ description: 'Language Server from Google',
+ minimumGoVersion: semver.coerce('1.12'),
+ latestVersion: semver.coerce('0.4.0'),
+ latestVersionTimestamp: moment('2020-04-08', 'YYYY-MM-DD'),
+ latestPrereleaseVersion: semver.coerce('0.4.1-pre2'),
+ latestPrereleaseVersionTimestamp: moment('2020-05-11', 'YYYY-MM-DD'),
},
'dlv': {
name: 'dlv',
diff --git a/src/stateUtils.ts b/src/stateUtils.ts
index 72402ac..c7b49c2 100644
--- a/src/stateUtils.ts
+++ b/src/stateUtils.ts
@@ -8,7 +8,7 @@
let globalState: vscode.Memento;
let workspaceState: vscode.Memento;
-export function getFromGlobalState(key: string, defaultValue?: any) {
+export function getFromGlobalState(key: string, defaultValue?: any): any {
if (!globalState) {
return defaultValue;
}
diff --git a/src/telemetry.ts b/src/telemetry.ts
index ad66384..d0e557e 100644
--- a/src/telemetry.ts
+++ b/src/telemetry.ts
@@ -6,10 +6,10 @@
import vscode = require('vscode');
import TelemetryReporter from 'vscode-extension-telemetry';
-export const extensionId: string = 'ms-vscode.Go';
+export const extensionId: string = 'golang.go-nightly';
const extension = vscode.extensions.getExtension(extensionId);
const extensionVersion: string = extension ? extension.packageJSON.version : '';
-const aiKey: string = 'AIF-d9b70cd4-b9f9-4d70-929b-a071c400b217';
+const aiKey: string = ''; // Empty aiKey disables telemetry.
export function sendTelemetryEventForModulesUsage() {
/* __GDPR__
diff --git a/src/util.ts b/src/util.ts
index 25ce19e..3e12248 100644
--- a/src/util.ts
+++ b/src/util.ts
@@ -330,7 +330,7 @@
case 1:
vendorSupport =
goVersion.sv.minor > 6 ||
- ((goVersion.sv.minor === 5 || goVersion.sv.minor === 6) && process.env['GO15VENDOREXPERIMENT'] === '1')
+ ((goVersion.sv.minor === 5 || goVersion.sv.minor === 6) && process.env['GO15VENDOREXPERIMENT'] === '1')
? true
: false;
break;
@@ -905,9 +905,14 @@
fs.readdirSync(dir).forEach((file) => {
const relPath = path.join(dir, file);
if (fs.lstatSync(relPath).isDirectory()) {
- rmdirRecursive(dir);
+ rmdirRecursive(relPath);
} else {
- fs.unlinkSync(relPath);
+ try {
+ fs.unlinkSync(relPath);
+ } catch (err) {
+ console.log(err);
+ }
+
}
});
fs.rmdirSync(dir);
diff --git a/test/gopls/extension.test.ts b/test/gopls/extension.test.ts
index bde1fd8..d355b3f 100644
--- a/test/gopls/extension.test.ts
+++ b/test/gopls/extension.test.ts
@@ -7,9 +7,7 @@
import * as fs from 'fs-extra';
import * as path from 'path';
import * as vscode from 'vscode';
-import { updateGoPathGoRootFromConfig } from '../../src/goInstallTools';
import { extensionId } from '../../src/telemetry';
-import { getCurrentGoPath } from '../../src/util';
// Env is a collection of test related variables
// that define the test environment such as vscode workspace.
@@ -65,7 +63,7 @@
files.filter((filename) => filename !== '.gitignore').map((file) => {
fs.remove(path.resolve(this.workspaceDir, file));
}));
- });
+ });
if (!fixtureDirName) {
return;
diff --git a/test/gopls/survey.test.ts b/test/gopls/survey.test.ts
new file mode 100644
index 0000000..bccd3d6
--- /dev/null
+++ b/test/gopls/survey.test.ts
@@ -0,0 +1,81 @@
+/*---------------------------------------------------------
+ * Copyright (C) Microsoft Corporation. All rights reserved.
+ * Licensed under the MIT License. See License.txt in the project root for license information.
+ *--------------------------------------------------------*/
+
+import * as assert from 'assert';
+import sinon = require('sinon');
+import { shouldPromptForGoplsSurvey, SurveyConfig } from '../../src/goLanguageServer';
+
+suite('gopls survey tests', () => {
+ test('prompt for survey', () => {
+ // global state -> offer survey
+ const testCases: [SurveyConfig, boolean][] = [
+ // User who is activating the extension for the first time.
+ [
+ {},
+ true,
+ ],
+ // User who has already taken the survey.
+ [
+ {
+ lastDateAccepted: new Date('2020-04-02'),
+ promptThisMonthTimestamp: new Date('2020-04-10'),
+ lastDatePrompted: new Date('2020-04-02'),
+ prompt: true,
+ promptThisMonth: false,
+ },
+ false,
+ ],
+ // User who has declined survey prompting.
+ [
+ {
+ promptThisMonthTimestamp: new Date('2020-04-10'),
+ lastDatePrompted: new Date('2020-04-02'),
+ prompt: false,
+ },
+ false,
+ ],
+ // User who hasn't activated the extension in a while, but has opted in to prompting.
+ [
+ {
+ promptThisMonthTimestamp: new Date('2019-04-10'),
+ lastDatePrompted: new Date('2019-01-02'),
+ prompt: true,
+ },
+ true,
+ ],
+ // User who hasn't activated the extension in a while, and has never been prompted.
+ [
+ {
+ promptThisMonthTimestamp: new Date('2019-04-10'),
+ lastDatePrompted: new Date('2019-01-02'),
+ },
+ true,
+ ],
+ // User who should get prompted this month, but hasn't been yet.
+ [
+ {
+ lastDateAccepted: undefined,
+ promptThisMonthTimestamp: new Date('2020-04-10'),
+ lastDatePrompted: new Date('2019-01-02'),
+ prompt: true,
+ promptThisMonth: true,
+ },
+ true,
+ ],
+ ];
+ testCases.map(([testConfig, wantPrompt], i) => {
+ // Replace Math.Random so that it always returns 1. This means
+ // that we will always choose to prompt, in the event that the
+ // user can be prompted that month.
+ sinon.replace(Math, 'random', () => 1);
+
+ const now = new Date('2020-04-29');
+ const gotPrompt = shouldPromptForGoplsSurvey(now, testConfig);
+ assert.equal(wantPrompt, gotPrompt, `prompt determination failed for ${i}`);
+
+ sinon.restore();
+ });
+ });
+});
diff --git a/test/gopls/update.test.ts b/test/gopls/update.test.ts
new file mode 100644
index 0000000..31f9ed9
--- /dev/null
+++ b/test/gopls/update.test.ts
@@ -0,0 +1,93 @@
+/*---------------------------------------------------------
+ * Copyright (C) Microsoft Corporation. All rights reserved.
+ * Licensed under the MIT License. See License.txt in the project root for license information.
+ *--------------------------------------------------------*/
+
+import * as assert from 'assert';
+import semver = require('semver');
+import sinon = require('sinon');
+import lsp = require('../../src/goLanguageServer');
+import { getTool, Tool } from '../../src/goTools';
+
+suite('gopls update tests', () => {
+ test('prompt for update', () => {
+ const tool = getTool('gopls');
+ const testCases: [string, string, boolean, semver.SemVer][] = [
+ ['outdated, tagged', 'v0.3.1', false, tool.latestVersion],
+ ['outdated, tagged (pre-release)', '0.3.1', true, tool.latestPrereleaseVersion],
+ ['up-to-date, tagged', 'v0.4.0', false, null],
+ ['up-to-date tagged (pre-release)', 'v0.4.0', true, tool.latestPrereleaseVersion],
+ ['developer version', '(devel)', false, null],
+ ['developer version (pre-release)', '(devel)', true, null],
+ ['nonsense version', 'nosuchversion', false, null],
+ ['nonsense version (pre-release)', 'nosuchversion', true, null],
+ [
+ 'latest pre-release',
+ 'v0.4.1-pre1 h1:w6e4AmFe6sDSVrgaRkf4WqLyVAlByUrr9QM5xH7z1e4=',
+ false, null,
+ ],
+ [
+ 'latest pre-release (pre-release)',
+ 'v0.4.1-pre1 h1:w6e4AmFe6sDSVrgaRkf4WqLyVAlByUrr9QM5xH7z1e4=',
+ true, null,
+ ],
+ [
+ 'outdated pre-release version',
+ 'v0.3.1-pre1 h1:pBnJjmdcHy5AiRJleOWaakxFHykf8uXzSZKQMd0EA0Q=',
+ false, tool.latestVersion,
+ ],
+ [
+ 'outdated pre-release version (pre-release)',
+ 'v0.3.1-pre1 h1:pBnJjmdcHy5AiRJleOWaakxFHykf8uXzSZKQMd0EA0Q=',
+ true, tool.latestPrereleaseVersion,
+ ],
+ [
+ 'recent pseudoversion after pre-release',
+ 'v0.0.0-20200509030707-2212a7e161a5 h1:0gSpZ0Z2URJoo3oilGRq9ViMLDTlmNSDCyeZNHHrvd4=',
+ false, null,
+ ],
+ [
+ 'recent pseudoversion before pre-release',
+ 'v0.0.0-20200501030707-2212a7e161a5 h1:0gSpZ0Z2URJoo3oilGRq9ViMLDTlmNSDCyeZNHHrvd4=',
+ false, null,
+ ],
+ [
+ 'recent pseudoversion before pre-release (pre-release)',
+ 'v0.0.0-20200501030707-2212a7e161a5 h1:0gSpZ0Z2URJoo3oilGRq9ViMLDTlmNSDCyeZNHHrvd4=',
+ true, tool.latestPrereleaseVersion,
+ ],
+ [
+ 'outdated pseudoversion',
+ 'v0.0.0-20200309030707-2212a7e161a5 h1:0gSpZ0Z2URJoo3oilGRq9ViMLDTlmNSDCyeZNHHrvd4=',
+ false, tool.latestVersion,
+ ],
+ [
+ 'outdated pseudoversion (pre-release)',
+ 'v0.0.0-20200309030707-2212a7e161a5 h1:0gSpZ0Z2URJoo3oilGRq9ViMLDTlmNSDCyeZNHHrvd4=',
+ true, tool.latestPrereleaseVersion,
+ ],
+ ];
+ testCases.map(async ([name, usersVersion, acceptPrerelease, want], i) => {
+ sinon.replace(lsp, 'getLocalGoplsVersion', async () => {
+ return usersVersion;
+ });
+ sinon.replace(lsp, 'getLatestGoplsVersion', async () => {
+ if (acceptPrerelease) {
+ return tool.latestPrereleaseVersion;
+ }
+ return tool.latestVersion;
+ });
+ sinon.replace(lsp, 'getTimestampForVersion', async (_: Tool, version: semver.SemVer) => {
+ if (version === tool.latestVersion) {
+ return tool.latestVersionTimestamp;
+ }
+ if (version === tool.latestPrereleaseVersion) {
+ return tool.latestPrereleaseVersionTimestamp;
+ }
+ });
+ const got = await lsp.shouldUpdateLanguageServer(tool, 'bad/path/to/gopls', true);
+ assert.equal(got, want, `${name}@${i} failed`);
+ sinon.restore();
+ });
+ });
+});
diff --git a/test/integration/extension.test.ts b/test/integration/extension.test.ts
index 34569ef..63043d5 100644
--- a/test/integration/extension.test.ts
+++ b/test/integration/extension.test.ts
@@ -32,6 +32,7 @@
import {
getBinPath,
getCurrentGoPath,
+ getGoConfig,
getGoVersion,
getImportPath,
getToolsGopath,
@@ -1310,7 +1311,7 @@
expected.length,
labels.length,
`expected number of completions: ${expected.length} Actual: ${labels.length} at position(${
- position.line + 1
+ position.line + 1
},${position.character + 1}) ${labels}`
);
expected.forEach((entry, index) => {
@@ -1435,67 +1436,44 @@
});
test('Build Tags checking', async () => {
- const config1 = Object.create(vscode.workspace.getConfiguration('go'), {
- vetOnSave: { value: 'off' },
- lintOnSave: { value: 'off' },
- buildOnSave: { value: 'package' },
- buildTags: { value: 'randomtag' }
- });
+ // Note: The following checks can't be parallelized because the underlying go build command
+ // runner (goBuild) will cancel any outstanding go build commands.
- const checkWithTags = check(vscode.Uri.file(path.join(fixturePath, 'buildTags', 'hello.go')), config1).then(
- (diagnostics) => {
- assert.equal(1, diagnostics.length, 'check with buildtag failed. Unexpected errors found');
- assert.equal(1, diagnostics[0].errors.length, 'check with buildtag failed. Unexpected errors found');
- assert.equal(diagnostics[0].errors[0].msg, 'undefined: fmt.Prinln');
- }
+ const checkWithTags = async (tags: string) => {
+ const fileUri = vscode.Uri.file(path.join(fixturePath, 'buildTags', 'hello.go'));
+ const defaultGoCfg = getGoConfig(fileUri);
+ const cfg = Object.create(defaultGoCfg, {
+ vetOnSave: { value: 'off' },
+ lintOnSave: { value: 'off' },
+ buildOnSave: { value: 'package' },
+ buildTags: { value: tags }
+ }) as vscode.WorkspaceConfiguration;
+
+ const diagnostics = await check(fileUri, cfg);
+ return ([] as string[]).concat(...diagnostics.map<string[]>((d) => {
+ return d.errors.map((e) => e.msg) as string[];
+ }));
+ };
+
+ const errors1 = await checkWithTags('randomtag');
+ assert.deepEqual(errors1, ['undefined: fmt.Prinln'], 'check with buildtag "randomtag" failed. Unexpected errors found.');
+
+ // TODO(hyangah): after go1.13, -tags expects a comma-separated tag list.
+ // For backwards compatibility, space-separated tag lists are still recognized,
+ // but change to a space-separated list once we stop testing with go1.12.
+ const errors2 = await checkWithTags('randomtag other');
+ assert.deepEqual(errors2, ['undefined: fmt.Prinln'],
+ 'check with multiple buildtags "randomtag,other" failed. Unexpected errors found.');
+
+ const errors3 = await checkWithTags('');
+ assert.equal(errors3.length, 1,
+ 'check without buildtag failed. Unexpected number of errors found' + JSON.stringify(errors3));
+ const errMsg = errors3[0];
+ assert.ok(
+ errMsg.includes(`can't load package: package test/testfixture/buildTags`) ||
+ errMsg.includes(`build constraints exclude all Go files`),
+ `check without buildtags failed. Go files not excluded. ${errMsg}`
);
-
- const config2 = Object.create(vscode.workspace.getConfiguration('go'), {
- vetOnSave: { value: 'off' },
- lintOnSave: { value: 'off' },
- buildOnSave: { value: 'package' },
- buildTags: { value: 'randomtag othertag' }
- });
-
- const checkWithMultipleTags = check(
- vscode.Uri.file(path.join(fixturePath, 'buildTags', 'hello.go')),
- config2
- ).then((diagnostics) => {
- assert.equal(1, diagnostics.length, 'check with multiple buildtags failed. Unexpected errors found');
- assert.equal(
- 1,
- diagnostics[0].errors.length,
- 'check with multiple buildtags failed. Unexpected errors found'
- );
- assert.equal(diagnostics[0].errors[0].msg, 'undefined: fmt.Prinln');
- });
-
- const config3 = Object.create(vscode.workspace.getConfiguration('go'), {
- vetOnSave: { value: 'off' },
- lintOnSave: { value: 'off' },
- buildOnSave: { value: 'package' },
- buildTags: { value: '' }
- });
-
- const checkWithoutTags = check(vscode.Uri.file(path.join(fixturePath, 'buildTags', 'hello.go')), config3).then(
- (diagnostics) => {
- assert.equal(1, diagnostics.length, 'check without buildtags failed. Unexpected errors found');
- assert.equal(
- 1,
- diagnostics[0].errors.length,
- 'check without buildtags failed. Unexpected errors found'
- );
- const errMsg = diagnostics[0].errors[0].msg;
- assert.equal(
- errMsg.includes(`can't load package: package test/testfixture/buildTags`) ||
- errMsg.includes(`build constraints exclude all Go files`),
- true,
- `check without buildtags failed. Go files not excluded. ${diagnostics[0].errors[0].msg}`
- );
- }
- );
-
- return Promise.all([checkWithTags, checkWithMultipleTags, checkWithoutTags]);
});
test('Test Tags checking', async () => {
diff --git a/test/integration/index.ts b/test/integration/index.ts
index 1184be9..12e0292 100644
--- a/test/integration/index.ts
+++ b/test/integration/index.ts
@@ -8,7 +8,7 @@
export function run(): Promise<void> {
// Create the mocha test
const mocha = new Mocha({
- ui: 'tdd'
+ ui: 'tdd',
});
mocha.useColors(true);
diff --git a/test/integration/install.test.ts b/test/integration/install.test.ts
new file mode 100644
index 0000000..778e5f2
--- /dev/null
+++ b/test/integration/install.test.ts
@@ -0,0 +1,96 @@
+/*---------------------------------------------------------
+ * Copyright (C) Microsoft Corporation. All rights reserved.
+ * Licensed under the MIT License. See License.txt in the project root for license information.
+ *--------------------------------------------------------*/
+
+import AdmZip = require('adm-zip');
+import * as assert from 'assert';
+import fs = require('fs');
+import os = require('os');
+import path = require('path');
+import sinon = require('sinon');
+import util = require('util');
+import vscode = require('vscode');
+import { installTools } from '../../src/goInstallTools';
+import { getTool, getToolAtVersion } from '../../src/goTools';
+import { getGoVersion, rmdirRecursive } from '../../src/util';
+
+suite('Installation Tests', () => {
+ test('install tools', async () => {
+ const goVersion = await getGoVersion();
+ const testCases: string[][] = [
+ ['gopls'],
+ ['gopls', 'guru'],
+ ];
+ const proxyDir = buildFakeProxy([].concat(...testCases));
+
+ for (const missing of testCases) {
+ // Create a temporary directory in which to install tools.
+ const tmpToolsGopath = fs.mkdtempSync(path.join(os.tmpdir(), 'install-test'));
+ fs.mkdirSync(path.join(tmpToolsGopath, 'bin'));
+ fs.mkdirSync(path.join(tmpToolsGopath, 'src'));
+
+ const sandbox = sinon.createSandbox();
+ const utils = require('../../src/util');
+ const toolsGopathStub = sandbox.stub(utils, 'getToolsGopath').returns(tmpToolsGopath);
+ const goConfig = Object.create(vscode.workspace.getConfiguration('go'), {
+ toolsEnvVars: {
+ value: {
+ GOPROXY: `file://${proxyDir}`,
+ GOSUMDB: 'off',
+ }
+ },
+ });
+ const configStub = sandbox.stub(vscode.workspace, 'getConfiguration').returns(goConfig);
+ // TODO(rstambler): Test with versions as well.
+ const missingTools = missing.map((tool) => getToolAtVersion(tool));
+ await installTools(missingTools, goVersion);
+
+ sinon.assert.calledWith(toolsGopathStub);
+ sinon.assert.calledWith(configStub);
+ sandbox.restore();
+
+ // Read the $GOPATH/bin to confirm that the expected tools were
+ // installed.
+ const readdir = util.promisify(fs.readdir);
+ const files = await readdir(path.join(tmpToolsGopath, 'bin'));
+ assert.deepEqual(files, missing, `tool installation failed for ${missing}`);
+
+ // TODO(rstambler): A module cache gets created in $GOPATH/pkg with
+ // different permissions, and fs.chown doesn't seem to work on it.
+ // Not sure how to remove the files so that the temporary directory
+ // can be deleted.
+ }
+
+ rmdirRecursive(proxyDir);
+ });
+});
+
+// buildFakeProxy creates a fake file-based proxy used for testing. The code is
+// mostly adapted from golang.org/x/tools/internal/proxydir/proxydir.go.
+function buildFakeProxy(tools: string[]) {
+ const proxyDir = fs.mkdtempSync(path.join(os.tmpdir(), 'proxydir'));
+ for (const toolName of tools) {
+ const tool = getTool(toolName);
+ const module = tool.importPath;
+ const version = `v1.0.0`; // hardcoded for now
+ const dir = path.join(proxyDir, module, '@v');
+ fs.mkdirSync(dir, { recursive: true });
+
+ // Write the list file.
+ fs.writeFileSync(path.join(dir, 'list'), `${version}\n`);
+
+ // Write the go.mod file.
+ fs.writeFileSync(path.join(dir, `${version}.mod`), `module ${module}\n`);
+
+ // Write the info file.
+ fs.writeFileSync(path.join(dir, `${version}.info`), `{ "Version": "${version}", "Time": "2020-04-07T14:45:07Z" } `);
+
+ // Write the zip file.
+ const zip = new AdmZip();
+ const content = `package main; func main() {};`;
+ zip.addFile(path.join(`${module}@${version}`, 'main.go'), Buffer.alloc(content.length, content));
+ zip.writeZip(path.join(dir, `${version}.zip`));
+ }
+ return proxyDir;
+}
diff --git a/test/runTest.ts b/test/runTest.ts
index ee29d48..b6dcd8a 100644
--- a/test/runTest.ts
+++ b/test/runTest.ts
@@ -8,6 +8,8 @@
// Passed to `--extensionDevelopmentPath`
const extensionDevelopmentPath = path.resolve(__dirname, '../../');
+ let failed = false;
+
try {
// The path to the extension test script
// Passed to --extensionTestsPath
@@ -17,7 +19,7 @@
await runTests({ extensionDevelopmentPath, extensionTestsPath });
} catch (err) {
console.error('Failed to run integration tests' + err);
- process.exit(1);
+ failed = true;
}
// Integration tests using gopls.
@@ -40,6 +42,11 @@
});
} catch (err) {
console.error('Failed to run gopls tests' + err);
+ // failed = true; TODO(hyangah): reenable this after golang.org/cl/233517
+ }
+
+ if (failed) {
+ process.exit(1);
}
}