diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index c93aba8..828ec18 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -7,52 +7,108 @@ assignees: '' --- -**Describe the bug** -A clear and concise description of what the bug is. +> DO NOT DELETE THIS TEMPLATE. IF YOU DELETE THIS TEMPLATE AND DO NOT COMPLETE IT, YOUR ISSUE WILL BE CLOSED. -**To Reproduce** -List of steps to reproduce +### Describe the bug -Vimspector config file: +> Provide A clear and concise description of what the bug is. + +### Minimal reproduciton + +> Please answer the following questions + +* Does your issue reproduce using `vim --clean -Nu /path/to/vimspector/support/minimal_vimrc` ? \[Yes/No] +* If you are using Neovim, does your issue reproduce using Vim? \[Yes/No] + +> List of steps to reproduce: + +> 1. Run `vim ---clean Nu /path/to/vimspector/support/minimal_vimrc` +> 2. Open _this project_... +> 3. Press _this sequence of keys_ + +> Use the following Vimspector config file: ``` paste .vimspector.json here ``` -**Expected behavior** -A clear and concise description of what you expected to happen. +### Expected behaviour -**Actual behaviour** -What actually happened, including output, log files etc. +> Provide A clear and concise description of what you expected to happen. -Please include: -* Vimspector log (~/.vimspector.log) -* Output from any or all UI diagnostic tabs (Server, etc.) +### Actual behaviour -**Environemnt** +> What actually happened, including output, log files etc. -NOTE: NeoVim is not supported. -NOTE: Windows is not supported. +> Please include: +> * Vimspector log (~/.vimspector.log) +> * Output from any or all UI diagnostic tabs (Server, etc.) -* Output of `vim --version` +### Environemnt + +***NOTE***: NeoVim is supported only on a best-effort basis. Please check the README +for limitations of neovim. Don't be offended if I ask you to reproduce issues in +Vim. + +***NOTE***: Windows support is experimental and best-efrort only. If you find an +issue related to Windows or windows-isms, consider sending a PR or +discussing on Gitter rather than raising an issue. + +* Version of Vimspector: (e.g. output of `git rev-parse HEAD` if cloned or the + name of the tarball used to install otherwise) + +* Output of `:VimspectorDebugInfo` ``` paste here ``` -* Output of `which vim`: +* Output of `vim --version` or `nvim --version` ``` paste here ``` -* Output of `:py3 pass`: +* Output of `which vim` or `which nvim`: + +``` +paste here +``` + +* Output of `:py3 print( __import__( 'sys' ).version )`: + +``` +paste here +``` + +* Output of `:py3 import vim`: + +``` +paste here +``` + +* Output of `:py3 import vimspector`: + +``` +paste here +``` + + +* For neovim: output of `:checkhealth` ``` paste here ``` * Operating system: and version + +### Declaration + +> Please complete the following declaration. If this declaration is not completed, your issue may be closed without comment. + +* I have read and understood [CONTRIBUTING.md](https://github.com/puremourning/vimspector/blob/master/CONTRIBUTING.md) \[Yes/No] + + +### Additional information -**Additional context** Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..f182c32 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,12 @@ +blank_issues_enabled: false +contact_links: + - name: Questions and support + url: http://gitter.im/vimspector/Lobby + about: Please ask and answer questions here. + - name: Discussions + url: https://github.com/puremourning/vimspector/discussions + about: Please post questions and useful hints here + - name: Support for additional languages + url: https://github.com/puremourning/vimspector/wiki/languages + about: Please see here for information on support for additional languages + diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml new file mode 100644 index 0000000..f186f5d --- /dev/null +++ b/.github/workflows/build.yaml @@ -0,0 +1,200 @@ +name: Build +on: + push: + branches: + - master + pull_request: + branches: + - master + +defaults: + run: + shell: bash + +jobs: + PythonLint: + runs-on: ubuntu-18.04 + container: 'puremourning/vimspector:test' + steps: + - uses: actions/checkout@v2 + - name: 'Insatll requirements' + run: pip3 install --user -r dev_requirements.txt + - name: 'Run flake8' + run: '$HOME/.local/bin/flake8 python3/ *.py' + VimscriptLint: + runs-on: 'ubuntu-18.04' + container: 'puremourning/vimspector:test' + steps: + - uses: actions/checkout@v2 + - name: 'Install requirements' + run: pip3 install --user -r dev_requirements.txt + - name: 'Run vint' + run: $HOME/.local/bin/vint autoload/ compiler/ plugin/ tests/ syntax/ + + Linux: + runs-on: 'ubuntu-18.04' + container: + image: 'puremourning/vimspector:test' + options: --cap-add=SYS_PTRACE --security-opt seccomp=unconfined + steps: + - uses: actions/checkout@v2 + + - run: | + eval $(/home/linuxbrew/.linuxbrew/bin/brew shellenv) + go get -u github.com/go-delve/delve/cmd/dlv + name: 'Install Delve for Go' + + - uses: actions/cache@v2 + with: + key: v1-gadgets-${{ runner.os }}-${{ hashFiles( 'python3/vimspector/gadgets.py' ) }} + path: gadgets/linux/download + name: Cache gadgets + + - run: vim --version + name: 'Print vim version information' + + - run: | + eval $(/home/linuxbrew/.linuxbrew/bin/brew shellenv) + export GOPATH=$HOME/go + ./run_tests --install --update --report messages --quiet + name: 'Run the tests' + id: run_tests + env: + VIMSPECTOR_MIMODE: gdb + + - name: "Upload test logs" + uses: actions/upload-artifact@v2 + if: failure() + with: + name: 'test-logs-${{ runner.os }}' + path: 'tests/logs/**/*' + + - run: ./make_package linux ${{ github.run_id }} + name: 'Package' + + # TODO: test the tarball + + - name: "Upload package" + uses: actions/upload-artifact@v2 + with: + name: 'package-linux' + path: 'package/linux-${{ github.run_id }}.tar.gz' + + # - name: Start SSH session if failed + # uses: luchihoratiu/debug-via-ssh@main + # if: failure() + # with: + # NGROK_AUTH_TOKEN: ${{ secrets.NGROK_AUTH_TOKEN }} + # SSH_PASS: ${{ secrets.SSH_PASS }} + + MacOS: + runs-on: 'macos-10.15' + steps: + - uses: actions/checkout@v2 + + - run: | + brew update-reset + brew doctor || true + for p in python@3.8 tcl-tk llvm lua luajit love; do + brew install $p || brew outdated $p || brew upgrade $p + done + brew install --cask macvim + brew link --overwrite python@3.8 + name: 'Install vim and deps' + + - run: go get -u github.com/go-delve/delve/cmd/dlv + name: 'Install Delve for Go' + + - uses: actions/cache@v2 + with: + key: v1-gadgets-${{ runner.os }}-${{ hashFiles( 'python3/vimspector/gadgets.py' ) }} + path: gadgets/macos/download + name: Cache gadgets + + - name: 'Install .NET Core SDK 3.1' + uses: actions/setup-dotnet@v1.7.2 + with: + dotnet-version: 3.1 + + - uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: ^11 + name: "Switch to xcode 11 because of .NET debugging bug" + + - run: vim --version + name: 'Print vim version information' + + - run: ./run_tests --install --update --report messages --quiet + name: 'Run the tests' + id: run_tests + env: + VIMSPECTOR_MIMODE: lldb + + - name: "Upload test logs" + uses: actions/upload-artifact@v2 + if: failure() + with: + name: 'test-logs-${{ runner.os }}' + path: 'tests/logs' + + - run: ./make_package macos ${{ github.run_id }} + name: 'Package' + + # TODO: test the tarball + + - name: "Upload package" + uses: actions/upload-artifact@v2 + with: + name: 'package-macos' + path: 'package/macos-${{ github.run_id }}.tar.gz' + + # - name: Start SSH session if failed + # uses: luchihoratiu/debug-via-ssh@main + # if: failure() + # with: + # NGROK_AUTH_TOKEN: ${{ secrets.NGROK_AUTH_TOKEN }} + # SSH_PASS: ${{ secrets.SSH_PASS }} # [V]imspector + + PublishRelease: + runs-on: 'ubuntu-18.04' + needs: + - Linux + - MacOS + if: github.ref == 'refs/heads/master' + steps: + - name: 'Download artifacts' + id: download_artifacts + uses: actions/download-artifact@v2 + + - name: 'Create Release' + id: create_release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ github.run_id }} + release_name: Build ${{ github.run_id }} + draft: false + prerelease: true + + - name: 'Upload Linux Package' + id: upload-release-asset-linux + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: ${{ steps.download_artifacts.outputs.download-path }}/package-linux/linux-${{ github.run_id }}.tar.gz + asset_name: vimspector-linux-${{ github.run_id }}.tar.gz + asset_content_type: application/gzip + + - name: 'Upload MacOS Package' + id: upload-release-asset-macos + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: ${{ steps.download_artifacts.outputs.download-path }}/package-macos/macos-${{ github.run_id }}.tar.gz + asset_name: vimspector-macos-${{ github.run_id }}.tar.gz + asset_content_type: application/gzip diff --git a/.github/workflows/lock_old_issues.yaml b/.github/workflows/lock_old_issues.yaml new file mode 100644 index 0000000..25d65e0 --- /dev/null +++ b/.github/workflows/lock_old_issues.yaml @@ -0,0 +1,27 @@ +name: "Lock Old Issues" + +on: + schedule: + - cron: '0 0 * * *' + workflow_dispatch: + +jobs: + lock: + runs-on: ubuntu-latest + steps: + - uses: dessant/lock-threads@v2 + with: + github-token: ${{ github.token }} + issue-lock-inactive-days: '60' + # issue-exclude-created-before: '' + # issue-exclude-labels: '' + # issue-lock-labels: '' + # issue-lock-comment: '' + # issue-lock-reason: 'resolved' + # pr-lock-inactive-days: '365' + # pr-exclude-created-before: '' + # pr-exclude-labels: '' + # pr-lock-labels: '' + # pr-lock-comment: '' + # pr-lock-reason: 'resolved' + process-only: 'issues' diff --git a/.gitignore b/.gitignore index 681e4fd..05dc8cd 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ tests/*.res tests/messages tests/debuglog test.log +*.testlog gadgets/ package/ *.pyc @@ -15,3 +16,8 @@ README.md.toc.* .DS_Store *.vimspector.log support/test/csharp/*.exe* +.neomake.log +configurations/ +venv/ +test-base/ +tags diff --git a/.mergify.yml b/.mergify.yml index a5fcafb..0f02002 100644 --- a/.mergify.yml +++ b/.mergify.yml @@ -3,11 +3,14 @@ pull_request_rules: conditions: - author=puremourning - base=master + # Review - status-success=code-review/reviewable - - status-success=puremourning.vimspector # Azure pipeline - - "#changes-requested-reviews-by=0" - + # CI https://doc.mergify.io/conditions.html#github-actions + - status-success=PythonLint + - status-success=VimscriptLint + - status-success=Linux + - status-success=MacOS actions: &merge-actions merge: method: merge @@ -18,12 +21,16 @@ pull_request_rules: conditions: - author!=puremourning - base=master + # Review - status-success=code-review/reviewable - - status-success=puremourning.vimspector # Azure pipeline - - approved-reviews-by=puremourning - - "#approved-reviews-by>=1" - "#changes-requested-reviews-by=0" + - approved-reviews-by=puremourning + # CI https://doc.mergify.io/conditions.html#github-actions + - status-success=PythonLint + - status-success=VimscriptLint + - status-success=Linux + - status-success=MacOS actions: <<: *merge-actions comment: diff --git a/.vimspector.json b/.vimspector.json index 5f7a93d..7e50c43 100644 --- a/.vimspector.json +++ b/.vimspector.json @@ -1,138 +1,22 @@ { - "adapters": { - "lldb-mi": { - "name": "lldb-mi", - "command": [ - "node", - "$HOME/.vscode/extensions/webfreak.debug-0.22.0/out/src/lldb.js" - ] - }, - "cppdbg": { - "name": "cppdbg", - "command": [ "$HOME/.vscode/extensions/ms-vscode.cpptools-0.20.1/debugAdapters/OpenDebugAD7" ], - "attach": { - "pidProperty": "processId", - "pidSelect": "ask" - } - }, - "python": { - "name": "python", - "command": [ - "node", - "$HOME/.vscode/extensions/ms-python.python-2018.4.0/out/client/debugger/Main.js" - ] - }, - "bashdb": { - "name": "bashdb", - "command": [ - "node", - "$HOME/.vscode/extensions/rogalmic.bash-debug-0.2.0/out/bashDebug.js" - ] - }, - "lldb": { - "name": "lldb", - "command": [ - "lldb", - "-b", - "-O", - "command script import '$HOME/.vscode/extensions/vadimcn.vscode-lldb-0.8.7/adapter'", - "-O", - "script adapter.main.run_stdio_session()" - ] - } - }, "configurations": { - "simple_c_program - lldb-mi Launch": { - "adapter": "lldb-mi", + "Python: Attach To Vim": { + "variables": { + "port": "5678", + "host": "localhost" + }, + "adapter": "multi-session", "configuration": { - "request": "launch", - "target": "support/test/cpp/simple_c_program/test", - "args": [], - "cwd": ".", - "lldbmipath": "$HOME/.vscode/extensions/ms-vscode.cpptools-0.20.1/debugAdapters/lldb/bin/lldb-mi", - "trace": true, - "logFilePath": "$HOME/.vimspector.protocol.log" + "request": "attach" } }, - "simple_c_progra - ms Launch": { - "adapter": "cppdbg", + "Python: Run current script": { + "adapter": "debugpy", "configuration": { - "name": "ms Launch", - "type": "cppdbg", "request": "launch", - "program": "${workspaceRoot}/support/test/cpp/simple_c_program/test", - "args": [], - "cwd": "$HOME", - "environment": [], - "externalConsole": true, - "MIMode": "lldb" - } - }, - "simple_python - launch": { - "adapter": "python", - "configuration": { - "name": "Python: Current File", - "type": "python", - "request": "launch", - "cwd": "${workspaceRoot}/support/test/python/simple_python", - "stopOnEntry": true, - "console": "externalTerminal", - "debugOptions": [], - "program": "${workspaceRoot}/support/test/python/simple_python/main.py" - } - }, - "simple_c_program - MS Attach": { - "adapter": "cppdbg", - "configuration": { - "name": "(lldb) Attach", - "type": "cppdbg", - "request": "attach", - "program": "${workspaceRoot}/support/test/cpp/simple_c_program/test", - "MIMode": "lldb" - } - }, - "bashdb": { - "adapter": "bashdb", - "configuration": { - "type": "bashdb", - "request": "launch", - "name": "Bash-Debug (simplest configuration)", - "program": "$HOME/.vim/bundle/YouCompleteMe/install.sh", - "args": [], - "cwd": "$HOME/.vim/bundle/YouCompleteMe", - "pathBash": "bash", - "pathBashdb": "bashdb", - "pathCat": "cat", - "pathMkfifo": "mkfifo", - "pathPkill": "pkill", - "showDebugOutput": true, - "trace": true - } - }, - "lldb launch": { - "adapter": "lldb", - "configuration": { - "type": "lldb", - "request": "launch", - "name": "LLDB: Launch", - "program": "$HOME/Development/vim/src/vim", - "args": [], - "cwd": "$HOME/Development/vim" - } - }, - "racerd": { - "adapter": "lldb", - "configuration": { - "type": "lldb", - "request": "launch", - "name": "LLDB: Launch", - "program": "$HOME/.vim/bundle/YouCompleteMe/third_party/ycmd/third_party/racerd/target/debug/racerd", - "args": [ - "serve", - "--port=12345", - "--secret-file=secretfile" - ], - "cwd": "$HOME/.vim/bundle/YouCompleteMe/third_party/ycmd" + "program": "${file}", + "args": [ "*${args:--update-gadget-config}" ], + "justMyCode#json": "${justMyCode:true}" } } } diff --git a/.vintrc.yml b/.vintrc.yml new file mode 100644 index 0000000..7e87977 --- /dev/null +++ b/.vintrc.yml @@ -0,0 +1,3 @@ +cmdargs: + color: true + severity: style_problem diff --git a/.ycm_extra_conf.py b/.ycm_extra_conf.py new file mode 100644 index 0000000..6f42505 --- /dev/null +++ b/.ycm_extra_conf.py @@ -0,0 +1,61 @@ +try: + from ycmd.extra_conf_support import IgnoreExtraConf +except ImportError: + IgnoreExtraConf = None + +import os.path as p + +PATH_TO_THIS_DIR = p.dirname( p.abspath( __file__ ) ) + + +def Settings( **kwargs ): + if kwargs[ 'language' ] == 'json': + return { + 'ls': { + 'json': { + 'schemas': [ + { + 'fileMatch': [ '.vimspector.json' ], + 'url': + f'file://{PATH_TO_THIS_DIR}/docs/schema/vimspector.schema.json' + }, + { + 'fileMatch': [ '.gadgets.json', '.gadgets.d/*.json' ], + 'url': + f'file://{PATH_TO_THIS_DIR}/docs/schema/gadgets.schema.json' + } + ] + } + }, + 'capabilities': { + 'textDocument': { + 'completion': { + 'completionItem': { + 'snippetSupport': True + } + } + } + } + } + + if kwargs[ 'language' ] == 'python': + return { + 'sys_path': [ + p.join( PATH_TO_THIS_DIR, 'python3' ) + ], + 'ls': { + 'python': { + 'analysis': { + 'extraPaths': [ + p.join( PATH_TO_THIS_DIR, 'python3' ), + ], + 'useLibraryCodeForTypes': True + } + } + } + } + + if IgnoreExtraConf: + raise IgnoreExtraConf() + + return None diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..0b7cfc3 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,128 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity and +orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the overall + community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or advances of + any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email address, + without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +`benjacksonbkg@gmail.com`. All complaints will be reviewed and investigated +promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series of +actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or permanent +ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within the +community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..adec034 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,235 @@ +# Contributing to Vimspector + +Contributions to Vimspector are always welcome. Contributions can take many +forms, such as: + +* Raising, responding to, or reacting to Issues or Pull Requests +* Testing new in-progress changes and providing feedback +* Discussing in the Gitter channel +* etc. + +At all times the [code of conduct](#code-of-conduct) applies. + +## Troubleshooting + +It's not completely trivial to configure Vimspector and there is a fairly large +amount of documentation. I know full well that documentation isn't everything, +so the first step in troubleshooting is to try a sample project that's known to +work, to check if the problem is your project configuration rather than an +actual bug. + +Therefore before raising an issue for a supported language, please check with +the sample projects in `support/test/` and `tests/testdata/` to see if +the problem is with your project settings, rather than with vimspector. + +Information on these is in [the README](README.md#trying-it-out). + +If in doubt, ask on Gitter. + +## Diagnostics + +Whenever reporting any type of fault, or difficulty in making the plugin +work, please always include _all_ of the diagnostics requested in the +[issue template][issue-template]. Please do not be offended if your request +is ignored if it does not include the requested diagnostics. + +The Vimspector log file contains a full trace of the communication between +Vimspector and the debug adapter. This is the primary source of diagnostic +information when something goes wrong that's not a clear Vim traceback. + +If you just want to see the Vimspector log file, use `:VimspectorToggleLog`, +which will tail it in a little window (doesn't work on Windows). + +## Issues + +The GitHub issue tracker is for *bug reports* and *features requests* for the +Vimspector project, and on-topic comments and follow-ups to them. It is not for +general discussion, general support or for any other purpose. + +Please **search the issue tracker for similar issues** before creating a new +one. There's no point in duplication; if an existing open issue addresses your +problem, please comment there instead of creating a duplicate. However, if the +issue you found is **closed as resolved** (e.g. with a PR or the original user's +problem was resolved), raise a **new issue**, because you've found a new +problem. Reference the original issue if you think that's useful information. + +Closed issues which have been inactive for 60 days will be locked, this helps to +keep discussions focussed. If you believe you are still experiencing an issue +which has been closed, please raise a new issue, completing the issue template. + +If you do find a similar _open_ issue, **don't just post 'me too' or similar** +responses. This almost never helps resolve the issue, and just causes noise for +the maintainers. Only post if it will aid the maintainers in solving the issue; +if there are existing diagnostics requested in the thread, perform +them and post the results. + +Please do not be offended if your Issue or comment is closed or hidden, for any +of the following reasons: + +* The [issue template][issue-template] was not completed +* The issue or comment is off-topic +* The issue does not represent a Vimspector bug or feature request +* The issue cannot be reasonably reproduced using the minimal vimrc +* The issue is a duplicate of an existing issue +* etc. + +Issue titles are important. It's not usually helpful to write a title like +`Issue with Vimspector` or `Issue configuring` or even pasting an error message. +Spend a minute to come up with a consise summary of the problem. This helps with +management of issues, with triage, and above all with searching. + +But above all else, please *please* complete the issue template. I know it is a +little tedious to get all the various diagnostics, but you *must* provide them, +*even if you think they are irrelevant*. This is important, because the +maintainer(s) can quickly cross-check theories by inspecting the provided +diagnostics without having to spend time asking for them, and waiting for the +response. This means *you get a better answer, faster*. So it's worth it, +honestly. + +### Reproduce your issue with the minimal vimrc + +Many problems can be caused by unexpected configuration or other plugins. +Therefore when raising an issue, you must attempt to reproduce your issue +with the minimal vimrc provided, and to provide any additional changes required +to that file in order to reproduce it. The purpose of this is to ensure that +the issue is not a conflict with another plugin, or a problem unique to your +configuration. + +If your issue does _not_ reproduce with the minimal vimrc, then you must say so +in the issue report. + +The minimal vimrc is in `support/test/minimal_vimrc` and can be used as follows: + +``` +vim --clean -Nu /path/to/vimspector/support/minimal_vimrc +``` + +## Pull Requests + +Vimspector is open to all contributors with ideas great and small! However, +there is a limit to the intended scope of the plugin and the amount of time the +maintainer has to support and... well... maintain features. It's probably well +understood that the contributor's input typically ends when a PR is megred, but +the maintainers have to keep it working forever. + +### Small changes + +For bug fixes, documentation changes, gadget versin updates, etc. please just +send a PR, I'm super happy to merge these! + +If you are unsure, or looking for some pointers, feel free to ask in Gitter, or +mention is in the PR. + +### Larger changes + +For larger features that might be in any way controvertial, or increase the +complexity of the overall plugin, please come to Gitter and talk to the +maintainer(s) first. This saves a lot of potential back-and-forth and makes sure +that we're "on the same page" about the idea and the ongoing maintenance. + +In addition, if you like hacking, feel free to raise a PR tagged with `[RFC]` in +the title and we can discuss the idea. I still prefer to discuss these things on +Gitter rather than back-and-forth on GitHub, though. + +Please don't be offended if the maintainer(s) request significant rework for (or +perhaps even dismiss) a PR that's not gone through this process. + +Please also don't be offended if the maintainer(s) ask if you're willing to +provide ongoing support for the feature. As an OSS project manned entirely in +what little spare time the maintainer(s) have, we're always looking for +contributions and contributors who will help with support and maintenance of +larger new features. + +### PR Guidelines + +When contributing pull requests, I ask that: + +* You provide a clear and complete summary of the change, the use case and how + the change was tested. +* You avoid using APIs that are not available in the versions listed in the + dependencies on README.md +* You add tests for your PR. +* You test your changes in both Vim and Neovim at the supported versions (and + state that in the PR). +* You follow the style of the code as-is; the python code is YCM-stye, it is + *not* PEP8, nor should it be. + +### Running the tests locally + +There are 2 ways: + +1. In the docker container. The CI tests for linux run in a container, so as to + ensure a consistent test environment. The container is defined in + `./tests/ci/`. There is also a container in `./tests/manual` which can be + used to run the tests interractively. To do this install and start docker, + then run `./tests/manual/run`. This will drop you into a bash shell inside + the linux container with your local vimspector code mounted. You can then + follow the instructions for running tets directly. +1. Directly: Run `./install_gadget.py --all` and then `./run_tests`. Note that + this depends on your runtime environment and might not match CI. I recommend + running the tests in the docker container. If you have your own custom + gadgets and/or custom configurations (in `vimspector/configurations` and/or + `vimspector/gadget`, then consider using `./run_tests --install --basedir + /tmp/vimspector_test` (then delete `/tmp/vimspector_test`). This will install + the gadgets to that dir and use it for the gadget dir/config dir so that your + custom configuration won't interfere with the tess. + +When tests fail, they dump a load of logs to a directory for each failed tests. +Usually the most useful output is `messages`, which tells you what actually +failed. + +For more infomration on the test framework, see +[this article](https://vimways.org/2019/a-test-to-attest-to/), authored by the +Vimspector creator. + +### Code Style + +The code style of the Python code is "YCM" style, because that's how I like it. +`flake8` is used to check for certain errors and code style. + +The code style of the Vimscript is largely the same, and it is linted by +`vint`. + +To run them: + +* (optional) Create and activate a virtual env: + `python3 -m venv venv ; source venv/bin/activate` +* Install the development dependencies: `pip install -r dev_requirements.txt` +* Run `flake8`: `flake8 python3/ *.py` +* Run `vint`: `vint autoload/ plugin/ tests/` + +They're also run by CI, so please check for lint failures. The canonical +definition of the command to run is the command run in CI, i.e. in +`.git/workflows/build.yml`. + +### Debugging Vimspector + +You can debug vimspector's python code using vimspector! We can use debugpy, +from within Vim's embedded python and connect to it. Here's how: + +1. In one instance of vim, run the following to get debugpy to start listening + for us to connect: `:py3 __import__( 'vimspector', fromlist=[ 'developer' ] + ).developer.SetUpDebugpy()` + +2. In another instance of Vim, set a breakpoint in the vimspector python code + you want to debug and launch vimspector (e.g. ``). Select the `Python: + attach to vim` profile. This will attach to the debugpy running in the other + vim. + +3. Back in the first vim (the debuggee), trigger the vimspector code in + question, e.g. by starting to debug something else. + +4. You'll see it pause, and the 2nd vim (the debugger), you should be able to + step through and inspect as with any other python remote debugging. + +NB. It's also possible to debug the vimscript code using vimspector, but this +requires unreleased vim patches and a fair amount of faff. You can always use +`:debug` (see the help) for this though. + +# Code of conduct + +Please see [code of conduct](CODE_OF_CONDUCT.md). + +[vint]: https://github.com/Vimjas/vint +[flake8]: https://flake8.pycqa.org/en/latest/ +[issue-template]: https://github.com/puremourning/vimspector/blob/master/.github/ISSUE_TEMPLATE/bug_report.md diff --git a/README.md b/README.md index 6a9d8f3..d198292 100644 --- a/README.md +++ b/README.md @@ -1,112 +1,184 @@ # vimspector - A multi language graphical debugger for Vim For a tutorial and usage overview, take a look at the -[Vimspector website][website] +[Vimspector website][website]. -[![Build Status](https://dev.azure.com/puremouron/Vimspector/_apis/build/status/puremourning.vimspector?branchName=master)](https://dev.azure.com/puremouron/Vimspector/_build/latest?definitionId=1&branchName=master) [![Gitter](https://badges.gitter.im/vimspector/Lobby.svg)](https://gitter.im/vimspector/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) +For detailed explanation of the `.vimspector.json` format, see the +[reference guide][vimspector-ref]. + +![Build](https://github.com/puremourning/vimspector/workflows/Build/badge.svg?branch=master) [![Gitter](https://badges.gitter.im/vimspector/Lobby.svg)](https://gitter.im/vimspector/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) - * [Features and Usage](#features-and-usage) - * [Supported debugging features](#supported-debugging-features) - * [Supported languages:](#supported-languages) - * [Languages known to work](#languages-known-to-work) - * [Other languages](#other-languages) - * [Installation](#installation) - * [Dependencies](#dependencies) - * [Language dependencies](#language-dependencies) - * [Clone the plugin](#clone-the-plugin) - * [Install some gadgets](#install-some-gadgets) - * [Manual gadget installation](#manual-gadget-installation) - * [The gadget directory](#the-gadget-directory) - * [About](#about) - * [Background](#background) - * [Status](#status) - * [Experimental](#experimental) - * [Mappings](#mappings) - * [Visual Studio / VSCode](#visual-studio--vscode) - * [Human Mode](#human-mode) - * [Usage](#usage) - * [Launch and attach by PID:](#launch-and-attach-by-pid) - * [Breakpoints](#breakpoints) - * [Stepping](#stepping) - * [Variables and scopes](#variables-and-scopes) - * [Watches](#watches) - * [Stack Traces](#stack-traces) - * [Program Output:](#program-output) - * [Console](#console) - * [Debug adapter configuration](#debug-adapter-configuration) - * [Supported Languages](#supported-languages-1) - * [Partially supported](#partially-supported) - * [Unsupported](#unsupported) - * [FAQ](#faq) - * [License](#license) + * [Features and Usage](#features-and-usage) + * [Supported debugging features](#supported-debugging-features) + * [Supported languages](#supported-languages) + * [Other languages](#other-languages) + * [Installation](#installation) + * [Quick Start](#quick-start) + * [Dependencies](#dependencies) + * [Neovim differences](#neovim-differences) + * [Windows differences](#windows-differences) + * [Trying it out](#trying-it-out) + * [Cloning the plugin](#cloning-the-plugin) + * [Install some gadgets](#install-some-gadgets) + * [VimspectorInstall and VimspectorUpdate commands](#vimspectorinstall-and-vimspectorupdate-commands) + * [install_gadget.py](#install_gadgetpy) + * [Manual gadget installation](#manual-gadget-installation) + * [The gadget directory](#the-gadget-directory) + * [Upgrade](#upgrade) + * [About](#about) + * [Background](#background) + * [Status](#status) + * [Experimental](#experimental) + * [Motivation](#motivation) + * [License](#license) + * [Sponsorship](#sponsorship) + * [Mappings](#mappings) + * [Visual Studio / VSCode](#visual-studio--vscode) + * [Human Mode](#human-mode) + * [Usage and API](#usage-and-api) + * [Launch and attach by PID:](#launch-and-attach-by-pid) + * [Launch with options](#launch-with-options) + * [Debug configuration selection](#debug-configuration-selection) + * [Get configurations](#get-configurations) + * [Breakpoints](#breakpoints) + * [Summary](#summary) + * [Line breakpoints](#line-breakpoints) + * [Conditional breakpoints](#conditional-breakpoints) + * [Exception breakpoints](#exception-breakpoints) + * [Clear breakpoints](#clear-breakpoints) + * [Run to Cursor](#run-to-cursor) + * [Stepping](#stepping) + * [Variables and scopes](#variables-and-scopes) + * [Variable or selection hover evaluation](#variable-or-selection-hover-evaluation) + * [Watches](#watches) + * [Watch autocompletion](#watch-autocompletion) + * [Stack Traces](#stack-traces) + * [Program Output](#program-output) + * [Console](#console) + * [Console autocompletion](#console-autocompletion) + * [Log View](#log-view) + * [Closing debugger](#closing-debugger) + * [Terminate debuggee](#terminate-debuggee) + * [Debug profile configuration](#debug-profile-configuration) + * [C, C++, Rust, etc.](#c-c-rust-etc) + * [Data visualization / pretty printing](#data-visualization--pretty-printing) + * [C++ Remote debugging](#c-remote-debugging) + * [C++ Remote launch and attach](#c-remote-launch-and-attach) + * [Rust](#rust) + * [Python](#python) + * [Python Remote Debugging](#python-remote-debugging) + * [Python Remote launch and attach](#python-remote-launch-and-attach) + * [TCL](#tcl) + * [C♯](#c) + * [Go](#go) + * [PHP](#php) + * [Debug web application](#debug-web-application) + * [Debug cli application](#debug-cli-application) + * [JavaScript, TypeScript, etc.](#javascript-typescript-etc) + * [Java](#java) + * [Hot code replace](#hot-code-replace) + * [Usage with YouCompleteMe](#usage-with-youcompleteme) + * [Other LSP clients](#other-lsp-clients) + * [Lua](#lua) + * [Other servers](#other-servers) + * [Customisation](#customisation) + * [Changing the default signs](#changing-the-default-signs) + * [Sign priority](#sign-priority) + * [Changing the default window sizes](#changing-the-default-window-sizes) + * [Changing the terminal size](#changing-the-terminal-size) + * [Custom mappings while debugging](#custom-mappings-while-debugging) + * [Advanced UI customisation](#advanced-ui-customisation) + * [Customising the WinBar](#customising-the-winbar) + * [Example](#example) + * [FAQ](#faq) - + # Features and Usage The plugin is a capable Vim graphical debugger for multiple languages. -It's mostly tested for c++ and python, but in theory supports any +It's mostly tested for C++, Python and TCL, but in theory supports any language that Visual Studio Code supports (but see caveats). The [Vimspector website][website] has an overview of the UI, along with basic instructions for configuration and setup. +But for now, here's a (rather old) screenshot of Vimspector debugging Vim: + +![vimspector-vim-screenshot](https://puremourning.github.io/vimspector-web/img/vimspector-overview.png) + +And a couple of brief demos: + +[![asciicast](https://asciinema.org/a/VmptWmFHTNLPfK3DVsrR2bv8S.svg)](https://asciinema.org/a/VmptWmFHTNLPfK3DVsrR2bv8S) + +[![asciicast](https://asciinema.org/a/1wZJSoCgs3AvjkhKwetJOJhDh.svg)](https://asciinema.org/a/1wZJSoCgs3AvjkhKwetJOJhDh) + ## Supported debugging features +- flexible configuration syntax that can be checked in to source control - breakpoints (function, line and exception breakpoints) +- conditional breakpoints (function, line) - step in/out/over/up, stop, restart +- run to cursor - launch and attach - remote launch, remote attach - locals and globals display -- watch expressions -- call stack and navigation -- variable value display hover -- interactive debug console -- launch debugee within Vim's embedded terminal +- watch expressions with autocompletion +- variable inspection tooltip on hover +- set variable value in locals, watch and hover windows +- call stack display and navigation +- hierarchical variable value display popup (see `VimspectorBalloonEval`) +- interactive debug console with autocompletion +- launch debuggee within Vim's embedded terminal - logging/stdout display +- simple stable API for custom tooling (e.g. integrate with language server) -## Supported languages: +## Supported languages -The following languages are used frequently by the author and are known to work -with little effort, and are supported as first-class languages. +The following table lists the languages that are "built-in" (along with their +runtime dependencies). They are categorised by their level of support: -- C, C++, etc. (languages supported by gdb or lldb) -- Python 2 and Python 3 -- TCL -- Bash scripts +* `Tested` : Fully supported, Vimspector regression tests cover them +* `Supported` : Fully supported, frequently used and manually tested +* `Experimental`: Working, but not frequently used and rarely tested +* `Legacy`: No longer supported, please migrate your config +* `Retired`: No longer included or supported. -## Languages known to work - -The following languages are used frequently by the author, but require some sort -of hackery that makes it challenging to support generally. These languages are -on a best-efforts basis: - -- Java (see caveats) -- C# (c-sharp) using dotnet core -- Go (requires separate installation of [Delve][]) -- Node.js (requires node <12 for installation) -- Anything running in chrome (i.e. javascript). - -## Languages known not to work - -- C# (c-sharp) using mono debug adapter (vimspector unable to set breakpoints) +| Language | Status | Switch (for `install_gadget.py`) | Adapter (for `:VimspectorInstall`) | Dependencies | +|--------------------|-----------|----------------------------------|------------------------------------|--------------------------------------------| +| C, C++, Rust etc. | Tested | `--all` or `--enable-c` (or cpp) | vscode-cpptools | mono-core | +| Rust, C, C++, etc. | Supported | `--force-enable-rust` | CodeLLDB | Python 3 | +| Python | Tested | `--all` or `--enable-python` | debugpy | Python 2.7 or Python 3 | +| Go | Tested | `--enable-go` | vscode-go | Node, Go, [Delve][] | +| TCL | Supported | `--all` or `--enable-tcl` | tclpro | TCL 8.5 | +| Bourne Shell | Supported | `--all` or `--enable-bash` | vscode-bash-debug | Bash v?? | +| Lua | Supported | `--all` or `--enable-lua` | local-lua-debugger-vscode | Node >=12.13.0, Npm, Lua interpreter | +| Node.js | Supported | `--force-enable-node` | vscode-node-debug2 | 6 < Node < 12, Npm | +| Javascript | Supported | `--force-enable-chrome` | debugger-for-chrome | Chrome | +| Java | Supported | `--force-enable-java ` | vscode-java-debug | Compatible LSP plugin (see [later](#java)) | +| C# (dotnet core) | Tested | `--force-enable-csharp` | netcoredbg | DotNet core | +| F#, VB, etc. | Supported | `--force-enable-[fsharp,vbnet]` | `, `--force-enable-vbnet` | netcoredbg | DotNet core | +| C# (mono) | _Retired_ | N/A | N/A | N/A | +| Python.legacy | _Retired_ | N/A | N/A | N/A | ## Other languages -Vimspector should work for any debug adapter that works in Visual Studio Code, -but there are certain limitations (see FAQ). If you're trying to get vimspector -to work with a language that's not "supported", head over to Gitter and contact -the author. It should be possible to get it going. +Vimspector should work for any debug adapter that works in Visual Studio Code. + +To use Vimspector with a language that's not "built-in", see this +[wiki page](https://github.com/puremourning/vimspector/wiki/languages). # Installation +## Quick Start + There are 2 installation methods: -* Using a release tarball, or -* Manually +* Using a release tarball and vim packages +* Using a clone of the repo (e.g. package manager) Release tarballs come with debug adapters for the default languages pre-packaged. To use a release tarball: @@ -116,7 +188,15 @@ pre-packaged. To use a release tarball: ``` $ mkdir -p $HOME/.vim/pack -$ curl -L | tar -C $HOME/.vim/pack zxvf - +$ curl -L | tar -C $HOME/.vim/pack zxvf - +``` + +3. Add `packadd! vimspector` to you `.vimrc` + +4. (optionally) Enable the default set of mappings: + +``` +let g:vimspector_enable_mappings = 'HUMAN' ``` 3. Configure your project's debug profiles (create `.vimspector.json`) @@ -125,62 +205,134 @@ Alternatively, you can clone the repo and select which gadgets are installed: 1. Check the dependencies 1. Install the plugin as a Vim package. See `:help packages`. -2. Install some 'gadgets' (debug adapters) +2. Add `packadd! vimspector` to you `.vimrc` +2. Install some 'gadgets' (debug adapters) - see `:VimspectorInstall ...` 3. Configure your project's debug profiles (create `.vimspector.json`) +If you prefer to use a plugin manager, see the plugin manager's docs. For +Vundle, use: + +```vim +Plugin 'puremourning/vimspector' +``` + +The following sections expand on the above brief overview. + ## Dependencies Vimspector requires: -* Vim version 8.1 with at least patch 1264 +* One of: + * Vim 8.2 Huge build compiled with Python 3.6 or later + * Neovim 0.4.3 with Python 3.6 or later (experimental) * One of the following operating systems: * Linux - * macOS Mojave or pater + * macOS Mojave or later + * Windows (experimental) Why such a new vim ? Well 2 reasons: -1. Because vimspector uses a lot of new Vim features +1. Because vimspector uses a lot of new Vim features 2. Because there are Vim bugs that vimspector triggers that will frustrate you if you hit them. -Why no Windows support? Because it's effort and it's not a priority for the -author. PRs are welcome. +Why is neovim experimental? Because the author doesn't use neovim regularly, and +there are no regression tests for vimspector in neovim, so it may break +occasionally. Issue reports are handled on best-efforts basis, and PRs are +welcome to fix bugs. See also the next section descibing differences for neovim +vs vim. -Which Linux versions? I only test on Ubuntu 18.04 and later and RHEL 6.5 and -RHEL 7.6. +Why Windows support experimental? Because it's effort and it's not a priority +for the author. PRs are welcome to fix bugs. Windows will not be regularly +tested. -## Language dependencies +Which Linux versions? I only test on Ubuntu 18.04 and later and RHEL 7. -The debug adapters themselves have certain runtime dependencies: +## Neovim differences -| Language | Status | Switch | Adapter | Dependencies | -|------------------|--------------|------------------------------|-------------------|------------------------| -| C, C++, etc. | Supported | `--all` or ` --enable-c` | vscode-cpptools | mono-core | -| Python | Supported | `--all` or `--enable-python` | vscode-python | Python 2.7 or Python 3 | -| TCL | Experimental | `--all` or `--enable-tcl` | tclpro | TCL 8.5 | -| Bourne Shell | Experimental | `--all` or `--enable-bash` | vscode-bash-debug | Bash v?? | -| C# (dotnet core) | Experimental | `--force-enable-csharp` | netcoredbg | DotNet core | -| C# (mono) | Experimental | `--force-enable-csharp` | vscode-mono-debug | Mono | -| Go | Experimental | `--enable-go` | vscode-go | Go, [Delve][] | -| Node.js | Experimental | `--force-enable-node` | vscode-node-debug2 | 6 < Node < 12, Npm | -| Javascript | Experimental | `--force-enable-chrome` | debugger-for-chrome | Chrome | +neovim doesn't implement some features Vimspector relies on: -For other languages, you'll need some other way to install the gadget. +* WinBar - used for the buttons at the top of the code window and for changing + the output window's current output. +* Prompt Buffers - used to send commands in the Console and add Watches. + (*Note*: prompt buffers are available in neovim nightly) +* Balloons - this allows for the variable evaluation popup to be displayed when + hovering the mouse. See below for how to create a keyboard mapping instead. -## Clone the plugin +Workarounds are in place as follows: + +* WinBar - There are [mappings](#mappings), + [`:VimspectorShowOutput`](#program-output) and + [`:VimspectorReset`](#closing-debugger) +* Prompt Buffers - There are [`:VimspectorEval`](#console) + and [`:VimspectorWatch`](#watches) +* Balloons - There is the `VimspectorBalloonEval` mapping. There is no +default mapping for this, so I recommend something like this to get variable +display in a popup: + +```viml +" mnemonic 'di' = 'debug inspect' (pick your own, if you prefer!) + +" for normal mode - the word under the cursor +nmap di VimspectorBalloonEval +" for visual mode, the visually selected text +xmap di VimspectorBalloonEval +``` + +## Windows differences + +The following features are not implemented for Windows: + +* Tailing the vimspector log in the Output Window. + +## Trying it out + +If you just want to try out vimspector without changing your vim config, there +are example projects for a number of languages in `support/test`, including: + +* Python (`support/test/python/simple_python`) +* Go (`support/test/go/hello_world` and `support/test/go/name-starts-with-vowel`) +* Nodejs (`support/test/node/simple`) +* Chrome (`support/test/chrome/`) +* etc. + +To test one of these out, cd to the directory and run: + +``` +vim -Nu /path/to/vimspector/tests/vimrc --cmd "let g:vimspector_enable_mappings='HUMAN'" +``` + +Then press ``. + +There's also a C++ project in `tests/testdata/cpp/simple/` with a `Makefile` +which can be used to check everything is working. This is used by the regression +tests in CI so should always work, and is a good way to check if the problem is +your configuration rather than a bug. + +## Cloning the plugin + +If you're not using a release tarball, you'll need to clone this repo to the +appropriate place. + +1. Clone the plugin There are many Vim plugin managers, and I'm not going to state a particular -preference, so if you choose to use one, you're on your own with installation -issues. +preference, so if you choose to use one, follow the plugin manager's +documentation. For example, for Vundle, use: -Install vimspector as a Vim package, either by cloning this repository into your -package path, like this: +```viml +Plugin 'puremourning/vimspector' +``` + +If you don't use a plugin manager already, install vimspector as a Vim package +by cloning this repository into your package path, like this: ``` $ git clone https://github.com/puremourning/vimspector ~/.vim/pack/vimspector/opt/vimspector ``` -2. Configure vimspector in your `.vimrc`: +2. Configure vimspector in your `.vimrc`, for example to enable the standard + mapings: ```viml let g:vimspector_enable_mappings = 'HUMAN' @@ -193,20 +345,35 @@ let g:vimspector_enable_mappings = 'HUMAN' packadd! vimspector ``` -See support/doc/example_vimrc.vim. - -Also, if you want to try out vimspector without changing your vim config, run: - -``` -vim -Nu /path/to/vimspector/tests/vimrc --cmd "let g:vimspector_enable_mappings='HUMAN'" -``` +See support/doc/example_vimrc.vim for a minimal example. ## Install some gadgets -There are a couple of ways of doing this, but ***using `install_gadget.py` is -highly recommended*** where that's an option. +Vimspector is a generic client for Debug Adapters. Debug Adapters (referred to +as 'gadgets' or 'adapters') are what actually do the work of talking to the real +debuggers. -For supported languages, `install_gadget.py` will: +In order for Vimspector to be useful, you need to have some adapters installed. + +There are a few ways to do this: + +* If you downloaded a tarball, gadgets for main supported languages are already + installed for you. +* Using `:VimspectorInstall ` (use TAB `wildmenu` to see the + options, also accepts any `install_gadget.py` option) +* Using `python3 install_gadget.py ` (use `--help` to see all options) +* Attempting to launch a debug configuration; if the configured adapter + can't be found, vimspector will suggest installing one. +* Using `:VimspectorUpdate` to install the latest supported versions of the + gadgets. + +Here's a demo of doing some installs and an upgrade: + +[![asciicast](https://asciinema.org/a/Hfu4ZvuyTZun8THNen9FQbTay.svg)](https://asciinema.org/a/Hfu4ZvuyTZun8THNen9FQbTay) + +Both `install_gadget.py` and `:VimspectorInstall` do the same set of things, +though the default behaviours are slightly different. For supported languages, +they will: * Download the relevant debug adapter at a version that's been tested from the internet, either as a 'vsix' (Visusal Studio plugin), or clone from GitHub. If @@ -214,43 +381,102 @@ For supported languages, `install_gadget.py` will: install the gadgets manually. * Perform any necessary post-installation actions, such as: * Building any binary components - * Ensuring scripts are executable, because the VSIX pacakges are usually + * Ensuring scripts are executable, because the VSIX packages are usually broken in this regard. * Set up the `gadgetDir` symlinks for the platform. -To install the tested debug adapter for a language, run: +For example, to install the tested debug adapter for a language, run: -``` -./install_gadget.py --enable- +| To install | Script | Command | +| --- | --- | --- | +| `` | | `:VimspectorInstall ` | +| ``, ``, ... | | `:VimspectorInstall ...` | +| `` | `./install_gadget.py --enable- ...` | `:VimspectorInstall --enable- ...` | +| Supported adapters | `./install_gadget.py --all` | `:VimspectorInstall --all` | +| Supported adapters, but not TCL | `./install_gadget.py --all --disable-tcl` | `:VimspectorInstall --all --disable-tcl` | +| Supported and experimental adapters | `./install_gadget.py --all --force-all` | `:VimspectorInstall --all` | +| Adapter for specific debug config | | Suggested by Vimspector when starting debugging | + +### VimspectorInstall and VimspectorUpdate commands + +`:VimspectorInstall` runs `install_gadget.py` in the background with some of +the options defaulted. + +`:VimspectorUpdate` runs `install_gadget.py` to re-install (i.e. update) any +gadgets already installed in your `.gadgets.json`. + +The output is minimal, to see the full output add `--verbose` to the command, as +in `:VimspectorInstall --verbose ...` or `:VimspectorUpdate --verbose ...`. + +If the installation is successful, the output window is closed (and the output +lost forever). Use a `!` to keep it open (e.g. `:VimspectorInstall! --verbose +--all` or `:VimspectorUpdate!` (etc.). + +If you know in advance which gadgets you want to install, for example so that +you can reproduce your config from source control, you can set +`g:vimspector_install_gadgets` to a list of gadgets. This will be used when: + +* Running `:VimspectorInstall` with no arguments, or +* Running `:VimspectorUpdate` + +For example: + +```viml +let g:vimspector_install_gadgets = [ 'debugpy', 'vscode-cpptools', 'CodeLLDB' ] ``` -Or to install all supported gagtets: +### install\_gadget.py -``` -./install_gadget.py --all +By default `install_gadget.py` will overwrite your `.gadgets.json` with the set +of adapters just installed, whereas `:VimspectorInstall` will _update_ it, +overwriting only newly changed or installed adapters. + +If you want to just add a new adapter using the script without destroying the +existing ones, add `--update-gadget-config`, as in: + +```bash +$ ./install_gadget.py --enable-tcl +$ ./install_gadget.py --enable-rust --update-gadget-config +$ ./install_gadget.py --enable-java --update-gadget-config ``` -To install everything other than TCL (because TCL is sadly not as popular as it -should be): +If you want to maintain `configurations` outside of the vimspector repository +(this can be useful if you have custom gadgets or global configurations), +you can tell the installer to use a different basedir, then set +`g:vimspector_base_dir` to point to that directory, for example: -``` -./install_gadget.py --all --disable-tcl +```bash +$ ./install_gadget.py --basedir $HOME/.vim/vimspector-config --all --force-all ``` -See `--help` for more info. +Then add this to your `.vimrc`: + +```viml +let g:vimspector_base_dir=expand( '$HOME/.vim/vimspector-config' ) +``` + +When usnig `:VimspectorInstall`, the `g:vimspector_base_dir` setting is +respected unless `--basedir` is manually added (not recommended). + +See `--help` for more info on the various options. ## Manual gadget installation +If the language you want to debug is not in the supported list above, you can +probably still make it work, but it's more effort. + You essentially need to get a working installation of the debug adapter, find out how to start it, and configure that in an `adapters` entry in either your `.vimspector.json` or in `.gadgets.json`. -The simplest way in practice is to install or start Visusal Studio Code and use +The simplest way in practice is to install or start Visual Studio Code and use its extension manager to install the relevant extension. You can then configure the adapter manually in the `adapters` section of your `.vimspector.json` or in -a `gagets.json`. +a `gadgets.json`. + +PRs are always welcome to add supported languages (which roughly translates to +updating `python/vimspector/gadgets.py` and testing it). -PRs are always welcome to add configuration to do this to `install_gadget.py`. ### The gadget directory @@ -266,35 +492,58 @@ Where os is one of: * `linux` * `windows` (though note: Windows is not supported) -The format is the same as `.vimspector.json`, but only the `gagets` key is used: +The format is the same as `.vimspector.json`, but only the `adapters` key is +used: Example: ```json { "adapters": { + "lldb-vscode": { + "variables": { + "LLVM": { + "shell": "brew --prefix llvm" + } + }, + "attach": { + "pidProperty": "pid", + "pidSelect": "ask" + }, + "command": [ + "${LLVM}/bin/lldb-vscode" + ], + "env": { + "LLDB_LAUNCH_FLAG_LAUNCH_IN_TTY": "YES" + }, + "name": "lldb" + }, "vscode-cpptools": { "attach": { - "pidProperty": "processId", + "pidProperty": "processId", "pidSelect": "ask" - }, + }, "command": [ "${gadgetDir}/vscode-cpptools/debugAdapters/OpenDebugAD7" - ], + ], "name": "cppdbg" - }, - "vscode-python": { - "command": [ - "node", - "${gadgetDir}/vscode-python/out/client/debugger/debugAdapter/main.js" - ], - "name": "vscode-python" } } } ``` -The gadget file is automatically written by `install_gadget.py`. +The gadget file is automatically written by `install_gadget.py` (or +`:VimspectorInstall`). + +Vimspector will also load any fies matching: +`/gadgets//.gadgets.d/*.json`. These have the same +format as `.gadgets.json` but are not overwritten when running +`install_gadget.py`. + +## Upgrade + +After updating the Vimspector code (either via `git pull` or whatever package +manager), run `:VimspectorUpdate` to update any already-installed gadgets. # About @@ -325,44 +574,119 @@ than welcome. The backlog can be [viewed on Trello](https://trello.com/b/yvAKK0rD/vimspector). -In order to use it you have to currently: - -- Write a mostly undocumented configuration file that contains essentially - undocumented parameters. -- Accept that it isn't complete yet -- Work around some frustrating bugs in Vim -- Ignore probably many bugs in vimspector! - ### Experimental The plugin is currently _experimental_. That means that any part of it can (and probably will) change, including things like: - breaking changes to the configuration -- keys, layout, functionatlity of the UI +- keys, layout, functionality of the UI -If a large number of people start using it then I will do my best to -minimise this, or at least announce on Gitter. +However, I commit to only doing this in the most extreme cases and to annouce +such changes on Gitter well in advance. There's nothing more annoying than stuff +just breaking on you. I get that. + +## Motivation + +A message from the author about the motivation for this plugin: + +> Many development environments have a built-in debugger. I spend an inordinate +> amount of my time in Vim. I do all my development in Vim and I have even +> customised my workflows for building code, running tests etc. +> +> For many years I have observed myself, friends and colleagues have been +> writing `printf`, `puts`, `print`, etc. debugging statements in all sorts of +> files simply because there is no _easy_ way to run a debugger for _whatever_ +> language we happen to be developing in. +> +> I truly believe that interactive, graphical debugging environments are the +> best way to understand and reason about both unfamiliar and familiar code, and +> that the lack of ready, simple access to a debugger is a huge hidden +> productivity hole for many. +> +> Don't get me wrong, I know there are literally millions of developers out +> there that are more than competent at developing without a graphical debugger, +> but I maintain that if they had the ability to _just press a key_ and jump +> into the debugger, it would be faster and more enjoyable that just cerebral +> code comprehension. +> +> I created Vimspector because I find changing tools frustrating. `gdb` for c++, +> `pdb` for python, etc. Each has its own syntax. Each its own lexicon. Each its +> own foibles. +> +> I designed the configuration system in such a way that the configuration can +> be committed to source control so that it _just works_ for any of your +> colleagues, friends, collaborators or complete strangers. +> +> I made remote debugging a first-class feature because that's a primary use +> case for me in my job. +> +> With Vimspector I can _just hit ``_ in all of the languages I develop in +> and debug locally or remotely using the exact same workflow, mappings and UI. +> I have integrated this with my Vim in such a way that I can hit a button and +> _run the test under the cursor in Vimspector_. This kind of integration has +> massively improved my workflow and productivity. It's even made the process +> of learning a new codebase... fun. +> +> \- Ben Jackson, Creator. + +## License + +[Apache 2.0](http://www.apache.org/licenses/LICENSE-2.0) + +Copyright © 2018 Ben Jackson + +## Sponsorship + +If you like Vimspector so much that you're wiling to part with your hard-earned cash, please consider donating to one of the following charities, which are meaningful to the author of Vimspector (in order of preference): + +* [Greyhound Rescue Wales](https://greyhoundrescuewales.co.uk) +* [Cancer Research UK](https://www.cancerresearchuk.org) +* [ICCF Holland](https://iccf.nl) +* Any charity of your choosing. # Mappings By default, vimspector does not change any of your mappings. Mappings are very personal and so you should work out what you like and use vim's powerful mapping -features to set your own mappings. For example, if you want `` to -start/continue debugging, add this to some appropriate place, such as your -`vimrc` (hint: run `:e $MYVIMRC`). +features to set your own mappings. To that end, Vimspector defines the following +`` mappings: + +| Mapping | Function | API | +| --- | --- | --- | +| `VimspectorContinue` | When debugging, continue. Otherwise start debugging. | `vimspector#Continue()` | +| `VimspectorStop` | Stop debugging. | `vimspector#Stop()` | +| `VimpectorRestart` | Restart debugging with the same configuration. | `vimspector#Restart()` | +| `VimspectorPause` | Pause debuggee. | `vimspector#Pause()` | +| `VimspectorToggleBreakpoint` | Toggle line breakpoint on the current line. | `vimspector#ToggleBreakpoint()` | +| `VimspectorToggleConditionalBreakpoint` | Toggle conditional line breakpoint on the current line. | `vimspector#ToggleBreakpoint( { trigger expr, hit count expr } )` | +| `VimspectorAddFunctionBreakpoint` | Add a function breakpoint for the expression under cursor | `vimspector#AddFunctionBreakpoint( '' )` | +| `VimspectorRunToCursor` | Run to Cursor | `vimspector#RunToCursor()` | +| `VimspectorStepOver` | Step Over | `vimspector#StepOver()` | +| `VimspectorStepInto` | Step Into | `vimspector#StepInto()` | +| `VimspectorStepOut` | Step out of current function scope | `vimspector#StepOut()` | +| `VimspectorUpFrame` | Move up a frame in the current call stack | `vimspector#UpFrame()` | +| `VimspectorDownFrame` | Move down a frame in the current call stack | `vimspector#DownFrame()` | +| `VimspectorBalloonEval` | Evaluate expression under cursor (or visual) in popup | *internal* | + + +These map roughly 1-1 with the API functions below. + +For example, if you want `` to start/continue debugging, add this to some +appropriate place, such as your `vimrc` (hint: run `:e $MYVIMRC`). ```viml -nnoremap :call vimspector#Continue() +nmap VimspectorContinue ``` +In addition, many users probably want to only enable certain Vimspector mappings +while debugging is active. This is also possible, though it requires writing +[some vimscipt](#custom-mappings-while-debugging). + That said, many people are familiar with particular debuggers, so the following mappings can be enabled by setting `g:vimspector_enable_mappings` to the specified value. -Please note: Currently there are no `` mappings. These will be added in -future to make custom mappings much easier. - ## Visual Studio / VSCode To use Visual Studio-like mappings, add the following to your `vimrc` **before @@ -372,17 +696,17 @@ loading vimspector**: let g:vimspector_enable_mappings = 'VISUAL_STUDIO' ``` -| Key | Function | API | -| --- | --- | --- | -| `F5` | When debugging, continue. Otherwise start debugging. | `vimspector#Continue()` | -| `Shift F5` | Stop debugging. | `vimspector#Stop()` | -| `Ctrl Shift F5` | Restart debugging with the same configuration. | `vimspector#Restart()` | -| `F6` | Pause debugee. | `vimspector#Pause()` | -| `F9` | Toggle line breakpoint on the current line. | `vimspector#ToggleBreakpoint()` | -| `Shift F9` | Add a function breakpoint for the expression under cursor | `vimspector#AddFunctionBreakpoint( '' )` | -| `F10` | Step Over | `vimspector#StepOver()` | -| `F11` | Step Into | `vimspector#StepInto()` | -| `Shift F11` | Step out of current function scope | `vimspector#StepOut()` | +| Key | Mapping | Function +| --- | --- | --- +| `F5` | `VimspectorContinue` | When debugging, continue. Otherwise start debugging. +| `Shift F5` | `VimspectorStop` | Stop debugging. +| `Ctrl Shift F5` | `VimspectorRestart` | Restart debugging with the same configuration. +| `F6` | `VimspectorPause` | Pause debuggee. +| `F9` | `VimspectorToggleBreakpoint` | Toggle line breakpoint on the current line. +| `Shift F9` | `VimspectorAddFunctionBreakpoint` | Add a function breakpoint for the expression under cursor +| `F10` | `VimspectorStepOver` | Step Over +| `F11` | `VimspectorStepInto` | Step Into +| `Shift F11` | `VimspectorStepOut` | Step out of current function scope ## Human Mode @@ -397,113 +721,434 @@ loading vimspector**: let g:vimspector_enable_mappings = 'HUMAN' ``` -| Key | Function | API | -| --- | --- | --- | -| `F5` | When debugging, continue. Otherwise start debugging. | `vimspector#Continue()` | -| `F3` | Stop debugging. | `vimspector#Stop()` | -| `F4` | Restart debugging with the same configuration. | `vimspector#Restart()` | -| `F6` | Pause debugee. | `vimspector#Pause()` | -| `F9` | Toggle line breakpoint on the current line. | `vimspector#ToggleBreakpoint()` | -| `F8` | Add a function breakpoint for the expression under cursor | `vimspector#AddFunctionBreakpoint( '' )` | -| `F10` | Step Over | `vimspector#StepOver()` | -| `F11` | Step Into | `vimspector#StepInto()` | -| `F12` | Step out of current function scope | `vimspector#StepOut()` | +| Key | Mapping | Function +| --- | --- | --- +| `F5` | `VimspectorContinue` | When debugging, continue. Otherwise start debugging. +| `F3` | `VimspectorStop` | Stop debugging. +| `F4` | `VimspectorRestart` | Restart debugging with the same configuration. +| `F6` | `VimspectorPause` | Pause debuggee. +| `F9` | `VimspectorToggleBreakpoint` | Toggle line breakpoint on the current line. +| `F9` | `VimspectorToggleConditionalBreakpoint` | Toggle conditional line breakpoint on the current line. +| `F8` | `VimspectorAddFunctionBreakpoint` | Add a function breakpoint for the expression under cursor +| `F8` | `VimspectorRunToCursor` | Run to Cursor +| `F10` | `VimspectorStepOver` | Step Over +| `F11` | `VimspectorStepInto` | Step Into +| `F12` | `VimspectorStepOut` | Step out of current function scope -# Usage +In addition, I recommend adding a mapping to `VimspectorBalloonEval`, in +normal and visual modes, for example: + +```viml +" mnemonic 'di' = 'debug inspect' (pick your own, if you prefer!) + +" for normal mode - the word under the cursor +nmap di VimspectorBalloonEval +" for visual mode, the visually selected text +xmap di VimspectorBalloonEval +``` + +You may also wish to add mappings for up/down the stack, for example: + +```viml +nmap VimspectorUpFrame +nmap VimspectorDownFrame +``` + +# Usage and API + +This section defines detailed usage instructions, organised by feature. For most +users, the [mappings](#mappings) section contains the most common commands and +default usage. This section can be used as a reference to create your own +mappings or custom behaviours. ## Launch and attach by PID: -* Create `vimspector.json`. See [below](#supported-languages). +* Create `.vimspector.json`. See [below](#supported-languages). * `:call vimspector#Launch()` and select a configuration. +![debug session](https://puremourning.github.io/vimspector-web/img/vimspector-overview.png) + +### Launch with options + +To launch a specific debug configuration, or specify [replacement +variables][vimspector-ref-var] for the launch, you can use: + +* `:call vimspector#LaunchWithSettings( dict )` + +The argument is a `dict` with the following keys: + +* `configuration`: (optional) Name of the debug configuration to launch +* ``: (optional) Name of a variable to set + +This allows for some integration and automation. For example, if you have a +configuration named `Run Test` that contains a [replacement +variable][vimspector-ref-var] named `${Test}` you could write a mapping which +ultimately executes: + +```viml +vimspector#LaunchWithSettings( #{ configuration: 'Run Test' + \ Test: 'Name of the test' } ) +``` + +This would start the `Run Test` configuration with `${Test}` set to `'Name of +the test'` and Vimspector would _not_ prompt the user to enter or confirm these +things. + +See [our YouCompleteMe integration guide](#usage-with-youcompleteme) for +another example where it can be used to specify the port to connect the [java +debugger](#java---partially-supported) + +### Debug configuration selection + +Vimspector uses the following logic to choose a configuration to launch: + +1. If a configuration was specified in the launch options (as above), use that. +2. Otherwise if there's only one configuration and it doesn't have `autoselect` + set to `false`, use that. +3. Otherwise if there's exactly one configuration with `default` set to `true` + and without `autoselect` set to `false`, use that. +4. Otherwise, prompt the user to select a configuration. + +See [the reference guide][vimspector-ref-config-selection] for details. + + +### Get configurations + +* Use `vimspector#GetConfigurations()` to get a list of configurations + +For example, to get an array of configurations and fuzzy matching on the result +```viml +:call matchfuzzy(vimspector#GetConfigurations(), "test::case_1") +``` + ## Breakpoints -* Use `vimspector#ToggleBreakpoint()` to set/disable/delete a line breakpoint. -* Use `vimspector#AddFunctionBreakpoint( '' )` to add a function -breakpoint. +See the [mappings](€mappings) section for the default mappings for working with +breakpoints. This section describes the full API in vimscript functions. + +### Summary + +* Use `vimspector#ToggleBreakpoint( { options dict } )` to set/disable/delete + a line breakpoint. The argument is optional (see below). +* Use `vimspector#AddFunctionBreakpoint( '', { options dict} )` + to add a function breakpoint. The second argument is optional (see below). +* Use `vimspector#SetLineBreakpoint( file_name, line_num, { options dict } )` to + set a breakpoint at a specific file/line. The last argument is optional + (see below) +* Use `vimspector#ClearLineBreakpoint( file_name, line_num )` to + remove a breakpoint at a specific file/line +* Use `vimspector#ClearBreakpoints()` to clear all breakpoints + +Examples: + +* `call vimspector#ToggleBreakpoint()` - toggle breakpoint on current line +* `call vimspector#SetLineBreakpoint( 'some_file.py', 10 )` - set a breakpoint + on `some_filepy:10` +* `call vimspector#AddFunctionBreakpoint( 'main' )` - add a function breakpoint + on the `main` function +* `call vimspector#ToggleBreakpoint( { 'condition': 'i > 5' } )` - add a + breakpoint on the current line that triggers only when `i > 5` is `true` +* `call vimspector#SetLineBreakpoint( 'some_file.py', 10, { 'condition': 'i > 5' } )` - add a + breakpoint at `some_file.py:10` that triggers only when `i > 5` is `true` +* `call vimspector#ClearLineBreakpoint( 'some_file.py', 10 )` - delete the + breakpoint at `some_file.py:10` +* `call vimspector#ClearBreakpoints()` - clear all breakpoints + +### Line breakpoints + +The simplest and most common form of breakpoint is a line breakpoint. Execution +is paused when the specified line is executed. + +For most debugging scenarios, users will just hit `` to create a line +breakpoint on the current line and `` to launch the application. + +### Conditional breakpoints + +Some debug adapters support conditional breakpoints. Note that vimspector does +not tell you if the debugger doesn't support conditional breakpoints (yet). A +conditional breakpoint is a breakpoint which only triggers if some expression +evaluates to true, or has some other constraints met. + +Some of these functions above take a single optional argument which is a +dictionary of options. The dictionary can have the following keys: + +* `condition`: An optional expression evaluated to determine if the breakpoint + should fire. Not supported by all debug adapters. For example, to break when + `abc` is `10`, enter something like `abc == 10`, depending on the language. +* `hitCondition`: An optional expression evaluated to determine a number of + times the breakpoint should be ignored. Should (probably?) not be used in + combination with `condition`. Not supported by all debug adapters. For + example, to break on the 3rd time hitting this line, enter `3`. + +In both cases, the expression is evaluated by the debugger, so should be in +whatever dialect the debugger understands when evaluating expressions. + +When using the `` mapping, the user is prompted to enter these +expressions in a command line (with history). + +### Exception breakpoints + +Exception breakpoints typically fire when an exception is throw or other error +condition occurs. Depending on the debugger, when starting debugging, you may be +asked a few questions about how to handle exceptions. These are "exception +breakpoints" and vimspector remembers your choices while Vim is still running. + +Typically you can accept the defaults (just keep pressing ``!) as most debug +adapter defaults are sane, but if you want to break on, say `uncaught exception` +then answer `Y` to that (for example). + +You can configure your choices in the `.vimspector.json`. See +[the configuration guide][vimspector-ref-exception] for details on that. + +### Clear breakpoints + +Use `vimspector#ClearBreakpoints()` +to clear all breakpoints including the memory of exception breakpoint choices. + +### Run to Cursor + +Use `vimspector#RunToCursor` or ``: this creates a temporary +breakpoint on the current line, then continues execution, clearing the +breakpoint when it is hit. ## Stepping -* Step in/out, finish, continue, pause etc. using the WinBar. +* Step in/out, finish, continue, pause etc. using the WinBar, or mappings. * If you really want to, the API is `vimspector#StepInto()` etc. +![code window](https://puremourning.github.io/vimspector-web/img/vimspector-code-window.png) + ## Variables and scopes * Current scope shows values of locals. -* Use `` to expand/collapse (+, -). +* Use ``, or double-click with left mouse to expand/collapse (+, -). +* Set the value of the variable with `` (control + ``) or + `` (if `modifyOtherKeys` doesn't work for you) * When changing the stack frame the locals window updates. * While paused, hover to see values +![locals window](https://puremourning.github.io/vimspector-web/img/vimspector-locals-window.png) + +Scopes and variables are represented by the buffer `vimspector.Variables`. + +## Variable or selection hover evaluation + +All rules for `Variables and scopes` apply plus the following: + +* With mouse enabled, hover over a variable and get the value it evaluates to. +* Use your mouse to perform a visual selection of an expression (e.g. `a + b`) + and get its result. +* Make a normal mode (`nmap`) and visual mode (`xmap`) mapping to + `VimspectorBalloonEval` to manually trigger the popup. + * Set the value of the variable with `` (control + ``) or + `` (if `modifyOtherKeys` doesn't work for you) + * Use regular nagivation keys (`j`, `k`) to choose the current selection; `` + (or leave the tooltip window) to close the tooltip. + +![variable eval hover](https://puremourning.github.io/vimspector-web/img/vimspector-variable-eval-hover.png) + ## Watches -The watches window is a prompt buffer. Enter insert mode to add a -new watch expression. +The watch window is used to inspect variables and expressions. Expressions are +evaluated in the selected stack frame which is "focussed" + +The watches window is a prompt buffer, where that's available. Enter insert mode +to add a new watch expression. * Add watches to the variables window by entering insert mode and typing the expression. Commit with ``. -* Expand result with ``. +* Alternatively, use `:VimspectorWatch `. Tab-completion for + expression is available in some debug adapters. +* Expand result with ``, or double-click with left mouse. +* Set the value of the variable with `` (control + ``) or + `` (if `modifyOtherKeys` doesn't work for you) * Delete with ``. +![watch window](https://puremourning.github.io/vimspector-web/img/vimspector-watch-window.png) + +The watches are represented by the buffer `vimspector.StackTrace`. + +### Watch autocompletion + +The watch prompt buffer has its `omnifunc` set to a function that will +calculate completion for the current expression. This is trivially used with +`` (see `:help ins-completion`), or integrated with your +favourite completion system. The filetype in the buffer is set to +`VimspectorPrompt`. + +For YouCompleteMe, the following config works well: + +```viml +let g:ycm_semantic_triggers = { + \ 'VimspectorPrompt': [ '.', '->', ':', '<' ] +} +``` + ## Stack Traces -* In the threads window, use `` to expand/collapse. -* Use `` on a stack frame to jump to it. +The stack trace window shows the state of each program thread. Threads which +are stopped can be expanded to show the stack trace of that thread. -## Program Output: +Often, but not always, all threads are stopped when a breakpoint is hit. The +status of a thread is show in parentheses after the thread's name. Where +supported by the underlying debugger, threads can be paused and continued +individually from within the Stack Trace window. -* In the outputs window use the WinBar to select the output channel. -* The debugee prints to the stdout channel. +A particular thread, highlighted with the `CursorLine` highlight group is the +"focussed" thread. This is the thread that receives commands like "Stop In", +"Stop Out", "Continue" and "Pause" in the code window. The focussed thread can +be changed manually to "switch to" that thread. + +* Use ``, or double-click with left mouse to expand/collapse a thread stack + trace, or use the WinBar button. +* Use ``, or double-click with left mouse on a stack frame to jump to it. +* Use the WinBar or `vimspector#PauseContinueThread()` to individually pause or + continue the selected thread. +* Use the "Focus" WinBar button, `` or `vimspector#SetCurrentThread()` + to set the "focussed" thread to the currently selected one. If the selected + line is a stack frame, set the focussed thread to the thread of that frame and + jump to that frame in the code window. +* The current frame when a breakpoint is hit or if manuall jumping is also + highlighted. + +![stack trace](https://puremourning.github.io/vimspector-web/img/vimspector-callstack-window.png) + +The stack trace is represented by the buffer `vimspector.StackTrace`. + +## Program Output + +* In the outputs window, use the WinBar to select the output channel. +* Alternatively, use `:VimspectorShowOutput `. Use command-line + completion to see the categories. +* The debuggee prints to the stdout channel. * Other channels may be useful for debugging. +![output window](https://puremourning.github.io/vimspector-web/img/vimspector-output-window.png) + +If the output window is closed, a new one can be opened with +`:VimspectorShowOutput ` (use tab-completion - `wildmenu` to see the +options). + ### Console -The console window is a prompt buffer and can be used as an interactive -CLI for the debug adapter. Support for this varies amongt adapters. +The console window is a prompt buffer, where that's available, and can be used +as an interactive CLI for the debug adapter. Support for this varies amongst +adapters. -* Enter insert mode to enter a command to evaluate +* Enter insert mode to enter a command to evaluate. +* Alternatively, `:VimspectorEval `. Completion is available with + some debug adapters. * Commit the request with `` * The request and subsequent result are printed. -NOTE: See also [Watches][#watches] above. +NOTE: See also [Watches](#watches) above. -# Debug adapter configuration +If the output window is closed, a new one can be opened with +`:VimspectorShowOutput Console`. -## Supported Languages +### Console autocompletion -For more information on the configuration of `.vimspector.json`, take a look at +The console prompt buffer has its `omnifunc` set to a function that will +calculate completion for the current command/expression. This is trivially used +with `` (see `:help ins-completion`), or integrated with your +favourite completion system. The filetype in the buffer is set to +`VimspectorPrompt`. + +For YouCompleteMe, the following config works well: + +```viml +let g:ycm_semantic_triggers = { + \ 'VimspectorPrompt': [ '.', '->', ':', '<' ] +} +``` + +### Log View + +The Vimspector log file contains a full trace of the communication between +Vimspector and the debug adapter. This is the primary source of diagnostic +information when something goes wrong that's not a Vim traceback. + +If you just want to see the Vimspector log file, use `:VimspectorToggleLog`, +which will tail it in a little window (doesn't work on Windows). + +You can see some debugging info with `:VimspectorDebugInfo` + +## Closing debugger + +To close the debugger, use: + +* `Reset` WinBar button +* `:VimspectorReset` when the WinBar is not available. +* `call vimspector#Reset()` + + +## Terminate debuggee + +If the debuggee is still running when stopping or resetting, then some debug +adapters allow you to specify what should happen to it when finishing debugging. +Typically, the default behaviour is sensible, and this is what happens most of +the time. These are the defaults according to DAP: + +* If the request was 'launch': terminate the debuggee +* If the request was 'attach': don't terminate the debuggee + +Some debug adapters allow you to choose what to do when disconnecting. If you +wish to control this behaviour, use `:VimspectorReset` or call +`vimspector#Reset( { 'interactive': v:true } )`. If the debug adapter offers a +choice as to whether or not to terminate the debuggee, you will be prompted to +choose. The same applies for `vimspector#Stop()` which can take an argument: +`vimspector#Stop( { 'interactive': v:true } )`. + + +# Debug profile configuration + +For an introduction to the configuration of `.vimspector.json`, take a look at the Getting Started section of the [Vimspector website][website]. +For full explanation, including how to use variables, substitutions and how to +specify exception breakpoints, see [the docs][vimspector-ref]. + +The JSON configuration file allows C-style comments: + +* `// comment to end of line ...` +* `/* inline comment ... */` + Current tested with the following debug adapters. -* C++: [vscode-cpptools](https://github.com/Microsoft/vscode-cpptools) +## C, C++, Rust, etc. -Example `.vimspector.json` +* [vscode-cpptools](https://github.com/Microsoft/vscode-cpptools) +* On macOS, I *strongly* recommend using [CodeLLDB](#rust) instead for C and C++ +projects. It's really excellent, has fewer dependencies and doesn't open console +apps in another Terminal window. + + +Example `.vimspector.json` (works with both `vscode-cpptools` and `lldb-vscode`. +For `lldb-vscode` replace the name of the adapter with `lldb-vscode`: + +* vscode-cpptools Linux/MacOS: ``` { "configurations": { - ": Launch": { + "Launch": { "adapter": "vscode-cpptools", "configuration": { - "name": "", - "type": "cppdbg", "request": "launch", "program": "", "args": [ ... ], "cwd": "", "environment": [ ... ], "externalConsole": true, - "MIMode": "lldb" + "MIMode": "" } }, - ": Attach": { + "Attach": { "adapter": "vscode-cpptools", "configuration": { - "name": ": Attach", - "type": "cppdbg", "request": "attach", "program": "", - "MIMode": "lldb" + "MIMode": "" } } ... @@ -511,18 +1156,155 @@ Example `.vimspector.json` } ``` -* Python: [vscode-python](https://github.com/Microsoft/vscode-python) +* vscode-cpptools Windows + +***NOTE FOR WINDOWS USERS:*** You need to install `gdb.exe`. I recommend using +`scoop install gdb`. Vimspector cannot use the visual studio debugger due to +licensing. ``` +{ + "configurations": { + "Launch": { + "adapter": "vscode-cpptools", + "configuration": { + "request": "launch", + "program": "", + "stopAtEntry": true + } + } + } +} +``` + +### Data visualization / pretty printing + +Depending on the backend you need to enable pretty printing of complex types manually. + +* LLDB: Pretty printing is enabled by default + +* GDB: To enable gdb pretty printers, consider the snippet below. + It is not enough to have `set print pretty on` in your .gdbinit! + +``` +{ + "configurations": { + "Launch": { + "adapter": "vscode-cpptools", + "configuration": { + "request": "launch", + "program": "", + ... + "MIMode": "gdb" + "setupCommands": [ + { + "description": "Enable pretty-printing for gdb", + "text": "-enable-pretty-printing", + "ignoreFailures": true + } + ], + } + } + } +} +``` + +### C++ Remote debugging + +The cpptools documentation describes how to attach cpptools to gdbserver using +`miDebuggerAddress`. Note that when doing this you should use the +`"request": "attach"`. + +### C++ Remote launch and attach + +If you're feeling fancy, checkout the [reference guide][remote-debugging] for +an example of getting Vimspector to remotely launch and attach. + +* CodeLLDB (MacOS) + +CodeLLDB is superior to vscode-cpptools in a number of ways on macOS at least. + +See [Rust](#rust). + +* lldb-vscode (MacOS) + +An alternative is to to use `lldb-vscode`, which comes with llvm. Here's how: + +* Install llvm (e.g. with HomeBrew: `brew install llvm`) +* Create a file named + `/path/to/vimspector/gadgets/macos/.gadgets.d/lldb-vscode.json`: + +```json +{ + "adapters": { + "lldb-vscode": { + "variables": { + "LLVM": { + "shell": "brew --prefix llvm" + } + }, + "attach": { + "pidProperty": "pid", + "pidSelect": "ask" + }, + "command": [ + "${LLVM}/bin/lldb-vscode" + ], + "env": { + "LLDB_LAUNCH_FLAG_LAUNCH_IN_TTY": "YES" + }, + "name": "lldb" + } + } +} +``` + +## Rust + +Rust is supported with any gdb/lldb-based debugger. So it works fine with +`vscode-cpptools` and `lldb-vscode` above. However, support for rust is best in +[`CodeLLDB`](https://github.com/vadimcn/vscode-lldb#features). + +* `./install_gadget.py --force-enable-rust` or `:VimspectorInstall CodeLLDB` +* Example: `support/test/rust/vimspector_test` + +```json +{ + "configurations": { + "launch": { + "adapter": "CodeLLDB", + "configuration": { + "request": "launch", + "program": "${workspaceRoot}/target/debug/vimspector_test" + } + } + } +} +``` + +* Docs: https://github.com/vadimcn/vscode-lldb/blob/master/MANUAL.md + + + +## Python + +* Python: [debugpy][] +* Install with `install_gadget.py --enable-python` or `:VimspectorInstall + debugpy`, ideally requires a working compiler and the python development + headers/libs to build a C python extension for performance. +* Full options: https://github.com/microsoft/debugpy/wiki/Debug-configuration-settings + +```json { "configurations": { ": Launch": { - "adapter": "vscode-python", + "adapter": "debugpy", "configuration": { "name": ": Launch", "type": "python", "request": "launch", "cwd": "", + "python": "/path/to/python/interpreter/to/use", "stopOnEntry": true, "console": "externalTerminal", "debugOptions": [], @@ -534,13 +1316,62 @@ Example `.vimspector.json` } ``` +### Python Remote Debugging + +In order to use remote debugging with debugpy, you have to connect Vimspector +directly to the application that is being debugged. This is easy, but it's a +little different from how we normally configure things. Specifically, you need +to: + + +* Start your application with debugpy, specifying the `--listen` argument. See + [the debugpy + documentation](https://github.com/microsoft/debugpy#debugpy-cli-usage) for + details. +* Use the built-in "multi-session" adapter. This just asks for the host/port to + connect to. For example: + +```json +{ + "configurations": { + "Python Attach": { + "adapter": "multi-session", + "configuration": { + "request": "attach", + "pathMappings": [ + // mappings here (optional) + ] + } + } + } +} +``` + +See [details of the launch +configuration](https://github.com/microsoft/debugpy/wiki/Debug-configuration-settings) +for explanation of things like `pathMappings`. + +Additional documentation, including how to do this when the remote machine can +only be contacted via SSH [are provided by +debugpy](https://github.com/microsoft/debugpy/wiki/Debugging-over-SSH). + +### Python Remote launch and attach + +If you're feeling fancy, checkout the [reference guide][remote-debugging] for +an example of getting Vimspector to remotely launch and attach. + +## TCL + * TCL (TclProDebug) See [my fork of TclProDebug](https://github.com/puremourning/TclProDebug) for instructions. +## C♯ + * C# - dotnet core -Requires `install_gadget.py --force-enable-c-sharp` +Install with `install_gadget.py --force-enable-csharp` or `:VimspectorInstall +netcoredbg` ```json { @@ -551,48 +1382,27 @@ Requires `install_gadget.py --force-enable-c-sharp` "request": "launch", "program": "${workspaceRoot}/bin/Debug/netcoreapp2.2/csharp.dll", "args": [], - "stopAtEntry": true - } - } - } -} -``` - -* C# - mono - -Requires `install_gadget.py --force-enable-c-sharp`. - -***Known not to work.**** - -```json -{ - "configurations": { - "launch - mono": { - "adapter": "vscode-mono-debug", - "configuration": { - "request": "launch", - "program": "${workspaceRoot}/bin/Debug/netcoreapp2.2/csharp.dll", - "args": [], + "stopAtEntry": true, "cwd": "${workspaceRoot}", - "runtimeExecutable": "mono", - "runtimeArgs": [], - "env": [], - "externalConsole": false, - "console": "integratedTerminal" + "env": {} } } } } ``` -* Go +## Go + +* Go Requires: -* `install_gadget.py --enable-go` +* `install_gadget.py --enable-go` or `:VimspectorInstall vscode-go` * [Delve][delve-install] installed, e.g. `go get -u github.com/go-delve/delve/cmd/dlv` * Delve to be in your PATH, or specify the `dlvToolPath` launch option +NOTE: Vimspector uses the ["legacy" vscode-go debug adapter](https://github.com/golang/vscode-go/blob/master/docs/debugging-legacy.md) rather than the "built-in" DAP support in Delve. You can track https://github.com/puremourning/vimspector/issues/186 for that. + ```json { "configurations": { @@ -609,12 +1419,91 @@ Requires: } ``` +See the vscode-go docs for +[troubleshooting information](https://github.com/golang/vscode-go/blob/master/docs/debugging-legacy.md#troubleshooting) + +## PHP + +This uses the php-debug, see +https://marketplace.visualstudio.com/items?itemName=felixfbecker.php-debug + +Requires: + +* (optional) Xdebug helper for chrome https://chrome.google.com/webstore/detail/xdebug-helper/eadndfjplgieldjbigjakmdgkmoaaaoc +* `install_gadget.py --force-enable-php` or `:VimspectorInstall + vscode-php-debug` +* configured php xdebug extension +```ini +zend_extension=xdebug.so +xdebug.remote_enable=on +xdebug.remote_handler=dbgp +xdebug.remote_host=localhost +xdebug.remote_port=9000 +``` +replace `localhost` with the ip of your workstation. + +lazy alternative +```ini +zend_extension=xdebug.so +xdebug.remote_enable=on +xdebug.remote_handler=dbgp +xdebug.remote_connect_back=true +xdebug.remote_port=9000 +``` + +* .vimspector.json +```json +{ + "configurations": { + "Listen for XDebug": { + "adapter": "vscode-php-debug", + "configuration": { + "name": "Listen for XDebug", + "type": "php", + "request": "launch", + "port": 9000, + "stopOnEntry": false, + "pathMappings": { + "/var/www/html": "${workspaceRoot}" + } + } + }, + "Launch currently open script": { + "adapter": "vscode-php-debug", + "configuration": { + "name": "Launch currently open script", + "type": "php", + "request": "launch", + "program": "${file}", + "cwd": "${fileDirname}", + "port": 9000 + } + } + } +} +``` + +### Debug web application +append `XDEBUG_SESSION_START=xdebug` to your query string +``` +curl "http://localhost?XDEBUG_SESSION_START=xdebug" +``` +or use the previously mentioned Xdebug Helper extension (which sets a `XDEBUG_SESSION` cookie) + +### Debug cli application +``` +export XDEBUG_CONFIG="idekey=xdebug" +php +``` + +## JavaScript, TypeScript, etc. + * Node.js Requires: * `install_gadget.py --force-enable-node` -* For installation, a Node.js environemt that is < node 12. I believe this is an +* For installation, a Node.js environment that is < node 12. I believe this is an incompatibility with gulp. Advice, use [nvm][] with `nvm install --lts 10; nvm use --lts 10; ./install_gadget.py --force-enable-node ...` * Options described here: @@ -646,7 +1535,8 @@ https://marketplace.visualstudio.com/items?itemName=msjsdiag.debugger-for-chrome It allows you to debug scripts running inside chrome from within Vim. -* `./install_gadget.py --force-enable-chrome` +* `./install_gadget.py --force-enable-chrome` or `:VimspectorInstall + debugger-for-chrome` * Example: `support/test/chrome` ```json @@ -664,32 +1554,506 @@ It allows you to debug scripts running inside chrome from within Vim. } ``` -Also the mock debugger, but that isn't actually useful. +## Java -## Partially supported +Vimspector works well with the [java debug server][java-debug-server], which +runs as a jdt.ls (Java Language Server) plugin, rather than a standalone +debug adapter. -* Java Debug Server. The [java debug server][java-debug-server] runs as a - jdt.ls plugin, rather than a standalone debug adapter. This makes a lot - of sense if you already happen to be running the language server. - Vimspector is not in the business of running language servers. So, rather - than doing so, vimspector simply allows you to start the java debug server - manually (however you might do so) and you can tell vimspector the port - on which it is listening. See [this issue](https://github.com/puremourning/vimspector/issues/3) - for more background. +Vimspector is not in the business of running language servers, only debug +adapters, so this means that you need a compatible Language Server Protocol +editor plugin to use Java. I recommend [YouCompleteMe][], which has full support +for jdt.ls, and most importantly a trivial way to load the debug adapter and to +use it with Vimspector. + +### Hot code replace + +When using the [java debug server][java-debug-server], Vimspector supports the +hot code replace custom feature. By default, when the underlying class files +change, vimspector asks the user if they wish to reload these classes at +runtime. + +This behaviour can be customised: + +* `let g:ycm_java_hotcodereplace_mode = 'ask'` - the default, ask the user for + each reload. +* `let g:ycm_java_hotcodereplace_mode = 'always'` - don't ask, always reload +* `let g:ycm_java_hotcodereplace_mode = 'never'` - don't ask, never reload + +### Usage with YouCompleteMe + +* Set up [YCM for java][YcmJava]. +* Get Vimspector to download the java debug plugin: + `install_gadget.py --force-enable-java ` or + `:VimspectorInstall java-debug-adapter` +* Configure Vimspector for your project using the `vscode-java` adapter, e.g.: + +```json +{ + "configurations": { + "Java Attach": { + "adapter": "vscode-java", + "configuration": { + "request": "attach", + "hostName": "${host}", + "port": "${port}", + "sourcePaths": [ + "${workspaceRoot}/src/main/java", + "${workspaceRoot}/src/test/java" + ] + } + } + } +} +``` + +* Tell YCM to load the debugger plugin. This should be the `gadgets/` + directory, not any specific adapter. e.g. in `.vimrc` + +```viml +" Tell YCM where to find the plugin. Add to any existing values. +let g:ycm_java_jdtls_extension_path = [ + \ '' + \ ] +``` + +* Create a mapping, such as `` to start the debug server and launch + vimspector, e.g. in `~/.vim/ftplugin/java.vim`: + +```viml +let s:jdt_ls_debugger_port = 0 +function! s:StartDebugging() + if s:jdt_ls_debugger_port <= 0 + " Get the DAP port + let s:jdt_ls_debugger_port = youcompleteme#GetCommandResponse( + \ 'ExecuteCommand', + \ 'vscode.java.startDebugSession' ) + + if s:jdt_ls_debugger_port == '' + echom "Unable to get DAP port - is JDT.LS initialized?" + let s:jdt_ls_debugger_port = 0 + return + endif + endif + + " Start debugging with the DAP port + call vimspector#LaunchWithSettings( { 'DAPPort': s:jdt_ls_debugger_port } ) +endfunction + +nnoremap :call StartDebugging() + +``` + +You can then use `` to start debugging rather than just ``. + +If you see "Unable to get DAP port - is JDT.LS initialized?", try running +`:YcmCompleter ExecuteCommand vscode.java.startDebugSession` and note the +output. If you see an error like `ResponseFailedException: Request failed: +-32601: No delegateCommandHandler for vscode.java.startDebugSession`, make sure +that: +* Your YCM jdt.ls is actually working, see the + [YCM docs](https://github.com/ycm-core/YouCompleteMe#troubleshooting) for + troubleshooting +* The YCM jdt.ls has had time to initialize before you start the debugger +* That `g:ycm_java_jdtls_extension_path` is set in `.vimrc` or prior to YCM + starting + +For the launch arguments, see the +[vscode document](https://code.visualstudio.com/docs/java/java-debugging). + +### Other LSP clients + +See [this issue](https://github.com/puremourning/vimspector/issues/3) for more +background. + +## Lua + +Lua is supported through +[local-lua-debugger-vscode](https://github.com/tomblind/local-lua-debugger-vscode). +This debugger uses stdio to communicate with the running process, so calls to +`io.read` will cause problems. + +* `./install_gadget.py --enable-lua` or `:VimspectorInstall local-lua-debugger-vscode` +* Examples: `support/test/lua/simple` and `support/test/lua/love` + +```json +{ + "$schema": "https://puremourning.github.io/vimspector/schema/vimspector.schema.json#", + "configurations": { + "lua": { + "adapter": "lua-local", + "configuration": { + "request": "launch", + "type": "lua-local", + "cwd": "${workspaceFolder}", + "program": { + "lua": "lua", + "file": "${file}" + } + } + }, + "luajit": { + "adapter": "lua-local", + "configuration": { + "request": "launch", + "type": "lua-local", + "cwd": "${workspaceFolder}", + "program": { + "lua": "luajit", + "file": "${file}" + } + } + }, + "love": { + "adapter": "lua-local", + "configuration": { + "request": "launch", + "type": "lua-local", + "cwd": "${workspaceFolder}", + "program": { + "command": "love" + }, + "args": ["${workspaceFolder}"] + } + } + } +} +``` + +## Other servers + +* Java - vscode-javac. This works, but is not as functional as Java Debug + Server. Take a look at [this + comment](https://github.com/puremourning/vimspector/issues/3#issuecomment-576916076) + for instructions. + + +# Customisation + +There is very limited support for customisation of the UI. + +## Changing the default signs + +Vimsector uses the following signs internally. If they are defined before +Vimsector uses them, they will not be replaced. So to customise the signs, +define them in your `vimrc`. + + +| Sign | Description | Priority | +|---------------------------|-----------------------------------------|----------| +| `vimspectorBP` | Line breakpoint | 9 | +| `vimspectorBPCond` | Conditional line breakpoint | 9 | +| `vimspectorBPDisabled` | Disabled breakpoint | 9 | +| `vimspectorPC` | Program counter (i.e. current line) | 200 | +| `vimspectorPCBP` | Program counter and breakpoint | 200 | +| `vimspectorCurrentThread` | Focussed thread in stack trace view | 200 | +| `vimspectorCurrentFrame` | Current stack frame in stack trace view | 200 | + +The default symbols are the equivalent of something like the following: + +```viml +sign define vimspectorBP text=\ ● texthl=WarningMsg +sign define vimspectorBPCond text=\ ◆ texthl=WarningMsg +sign define vimspectorBPDisabled text=\ ● texthl=LineNr +sign define vimspectorPC text=\ ▶ texthl=MatchParen linehl=CursorLine +sign define vimspectorPCBP text=●▶ texthl=MatchParen linehl=CursorLine +sign define vimspectorCurrentThread text=▶ texthl=MatchParen linehl=CursorLine +sign define vimspectorCurrentFrame text=▶ texthl=Special linehl=CursorLine +``` + +If the signs don't display properly, your font probably doesn't contain these +glyphs. You can easily change them by defining the sign in your vimrc. For +example, you could put this in your `vimrc` to use some simple ASCII symbols: + +```viml +sign define vimspectorBP text=o texthl=WarningMsg +sign define vimspectorBPCond text=o? texthl=WarningMsg +sign define vimspectorBPDisabled text=o! texthl=LineNr +sign define vimspectorPC text=\ > texthl=MatchParen +sign define vimspectorPCBP text=o> texthl=MatchParen +sign define vimspectorCurrentThread text=> texthl=MatchParen +sign define vimspectorCurrentFrame text=> texthl=Special +``` + +## Sign priority + +Many different plugins provide signs for various purposes. Examples include +diagnostic signs for code errors, etc. Vim provides only a single priority to +determine which sign should be displayed when multiple signs are placed at a +single line. If you are finding that other signs are interfering with +vimspector's (or vice-versa), you can customise the priority used by vimspector +by setting the following dictionary: + +```viml +let g:vimspector_sign_priority = { + \ '': , + \ } +``` + +For example: + +```viml +let g:vimspector_sign_priority = { + \ 'vimspectorBP': 3, + \ 'vimspectorBPCond': 2, + \ 'vimspectorBPDisabled': 1, + \ 'vimspectorPC': 999, + \ } +``` + +All keys are optional. If a sign is not customised, the default priority it used +(as shown above). + +See `:help sign-priority`. The default priority is 10, larger numbers override +smaller ones. + +## Changing the default window sizes + +> ***Please Note***: This customisation API is ***unstable***, meaning that it may +change at any time. I will endeavour to reduce the impact of this and announce +changes in Gitter. + +The following options control the default sizes of the UI windows (all of them +are numbers) + +- `g:vimspector_sidebar_width` (default: 50 columns): + The width in columns of the left utility windows (variables, watches, stack + trace) +- `g:vimspector_bottombar_height` (default 10 lines): + The height in rows of the output window below the code window. + +Example: + +```viml +let g:vimspector_sidebar_width = 75 +let g:vimspector_bottombar_height = 15 +``` + +## Changing the terminal size + +The terminal is typically created as a vertical split to the right of the code +window, and that window is re-used for subsequent terminal buffers. +The following control the sizing of the terminal window used +for debuggee input/output when using Vim's built-in terminal. + +- `g:vimspector_code_minwidth` (default: 82 columns): + Minimum number of columns to try and maintain for the code window when + splitting to create the terminal window. +- `g:vimspector_terminal_maxwidth` (default: 80 columns): + Maximum number of columns to use for the terminal. +- `g:vimspector_terminal_minwidth` (default: 10 columns): + Minimum number of columns to use when it is not possible to fit + `g:vimspector_terminal_maxwidth` columns for the terminal. + +That's a lot of options, but essentially we try to make sure that there are at +least `g:vimspector_code_minwidth` columns for the main code window and that the +terminal is no wider than `g:vimspector_terminal_maxwidth` columns. +`g:vimspector_terminal_minwidth` is there to ensure that there's a reasonable +number of columns for the terminal even when there isn't enough horizontal space +to satisfy the other constraints. + +Example: + +```viml +let g:vimspector_code_minwidth = 90 +let g:vimspector_terminal_maxwidth = 75 +let g:vimspector_terminal_minwidth = 20 +``` + +## Custom mappings while debugging + +It's useful to be able to define mappings only while debugging and remove those +mappings when debugging is complete. For this purpose, Vimspector provides 2 +`User` autocommands: + +* `VimspectorJumpedToFrame` - triggered whenever a 'break' event happens, or + when selecting a stack from to jump to. This can be used to create (for + example) buffer-local mappings for any files opened in the code window. +* `VimspectorDebugEnded` - triggered when the debug session is terminated + (actually when Vimspector is fully reset) + +An example way to use this is included in `support/custom_ui_vimrc`. In there, +these autocommands are used to create buffer-local mappings for any files +visited while debugging and to clear them when completing debugging. This is +particularly useful for commands like `VimspectorBalloonEval` which only +make sense while debugging (and only in the code window). Check the commented +section `Custom mappings while debugging`. + +NOTE: This is a fairly advanced feature requiring some nontrivial vimscript. +It's possible that this feature will be incorporated into Vimspector in future +as it is a common requirement. + +## Advanced UI customisation + +> ***Please Note***: This customisation API is ***unstable***, meaning that it may +change at any time. I will endeavour to reduce the impact of this and announce +changes in Gitter. + +The above customisation of window sizes is limited intentionally to keep things +simple. Vimspector also provides a way for you to customise the UI without +restrictions, by running a `User` autocommand just after creating the UI or +opening the terminal. This requires you to write some vimscript, but allows you +to do things like: + +* Hide a particular window or windows +* Move a particular window or windows +* Resize windows +* Have multiple windows for a particular buffer (say, you want 2 watch windows) +* etc. + +You can essentially do anything you could do manually by writing a little +vimscript code. + +The `User` autocommand is raised with `pattern` set with the following values: + +* `VimspectorUICreated`: Just after setting up the UI for a debug session +* `VimspectorTerminalOpened`: Just after opening the terminal window for program + input/output. + +The following global variable is set up for you to get access to the UI +elements: `g:vimspector_session_windows`. This is a `dict` with the following +keys: + +* `g:vimspector_session_windows.tagpage`: The tab page for the session +* `g:vimspector_session_windows.variables`: Window ID of the variables window, + containing the `vimspector.Variables` buffer. +* `g:vimspector_session_windows.watches`: Window ID of the watches window, + containing the `vimspector.Watches` buffer. +* `g:vimspector_session_windows.stack_trace`: Window ID of the stack trade + window containing the `vimspector.StackTrace` buffer. +* `g:vimspector_session_windows.code`: Window ID of the code window. +* `g:vimspector_session_windows.output`: Window ID of the output window. + +In addition, the following key is added when triggering the +`VimspectorTerminalOpened` event: + +* `g:vimspector_session_windows.terminal`: Window ID of the terminal window + +## Customising the WinBar + +You can even customise the WinBar buttons by simply running the usual `menu` +(and `unmenu`) commands. + +By default, Vimspector uses something a bit like this: + +```viml +nnoremenu WinBar.■\ Stop :call vimspector#Stop( { 'interactive': v:false } ) +nnoremenu WinBar.▶\ Cont :call vimspector#Continue() +nnoremenu WinBar.▷\ Pause :call vimspector#Pause() +nnoremenu WinBar.↷\ Next :call vimspector#StepOver() +nnoremenu WinBar.→\ Step :call vimspector#StepInto() +nnoremenu WinBar.←\ Out :call vimspector#StepOut() +nnoremenu WinBar.⟲: :call vimspector#Restart() +nnoremenu WinBar.✕ :call vimspector#Reset( { 'interactive': v:false } ) +``` + +If you prefer a different layout or if the unicode symbols don't render +correctly in your font, you can customise this in the `VimspectorUICreated` +autocommand, for example: + +```viml +func! CustomiseUI() + call win_gotoid( g:vimspector_session_windows.code ) + " Clear the existing WinBar created by Vimspector + nunmenu WinBar + " Cretae our own WinBar + nnoremenu WinBar.Kill :call vimspector#Stop( { 'interactive': v:true } ) + nnoremenu WinBar.Continue :call vimspector#Continue() + nnoremenu WinBar.Pause :call vimspector#Pause() + nnoremenu WinBar.Step\ Over :call vimspector#StepOver() + nnoremenu WinBar.Step\ In :call vimspector#StepInto() + nnoremenu WinBar.Step\ Out :call vimspector#StepOut() + nnoremenu WinBar.Restart :call vimspector#Restart() + nnoremenu WinBar.Exit :call vimspector#Reset() +endfunction + +augroup MyVimspectorUICustomistaion + autocmd! + autocmd User VimspectorUICreated call s:CustomiseUI() +augroup END +``` + +## Example + +There is some example code in `support/custom_ui_vimrc` showing how you can use +the window IDs to modify various aspects of the UI using some basic vim +commands, primarily `win_gotoid` function and the `wincmd` ex command. + +To try this out `vim -Nu support/custom_ui_vimrc `. + +Here's a rather smaller example. A simple way to use this is to drop it into a +file named `my_vimspector_ui.vim` in `~/.vim/plugin` (or paste into your +`vimrc`): + +```viml +" Set the basic sizes +let g:vimspector_sidebar_width = 80 +let g:vimspector_code_minwidth = 85 +let g:vimspector_terminal_minwidth = 75 + +function! s:CustomiseUI() + " Customise the basic UI... + + " Close the output window + call win_gotoid( g:vimspector_session_windows.output ) + q +endfunction + +function s:SetUpTerminal() + " Customise the terminal window size/position + " For some reasons terminal buffers in Neovim have line numbers + call win_gotoid( g:vimspector_session_windows.terminal ) + set norelativenumber nonumber +endfunction + +augroup MyVimspectorUICustomistaion + autocmd! + autocmd User VimspectorUICreated call s:CustomiseUI() + autocmd User VimspectorTerminalOpened call s:SetUpTerminal() +augroup END +``` # FAQ -1. Q: Does it work? A: Yeah, sort of. It's _incredibly_ buggy and unpolished. +1. Q: Does it work? A: Yeah. It's a bit unpolished. 2. Q: Does it work with _this_ language? A: Probably, but it won't necessarily be easy to work out what to put in the `.vimspector.json`. As you can see above, some of the servers aren't really editor agnostic, and require very-specific unique handling. +3. How do I stop it starting a new Terminal.app on macOS? See [this + comment](https://github.com/puremourning/vimspector/issues/90#issuecomment-577857322) +4. Can I specify answers to the annoying questions about exception breakpoints + in my `.vimspector.json` ? Yes, see [here][vimspector-ref-exception]. +5. Do I have to specify the file to execute in `.vimspector.json`, or could it be the current vim file? + You don't need to. You can specify $file for the current active file. See [here][vimspector-ref-var] for complete list of replacements in the configuration file. +6. You allow comments in `.vimspector.json`, but Vim highlights these as errors, + do you know how to make this not-an-error? Yes, put this in + `~/.vim/after/syntax/json.vim`: -# License +```viml +syn region jsonComment start="/\*" end="\*/" +hi link jsonCommentError Comment +hi link jsonComment Comment +``` -[Apache 2.0](http://www.apache.org/licenses/LICENSE-2.0) +7. What is the difference between a `gadget` and an `adapter`? A gadget is + something you install with `:VimspectorInstall` or `install_gadget.py`, an + `adapter` is something that Vimspector talks to (actually it's the Vimspector + config describing that thing). These are _usually_ one-to-one, + but in theory a single gadget can supply multiple `adapter` configs. + Typically this happens when a `gadget` supplies different `adapter` config + for, say remote debugging, or debugging in a container, etc. +8. The signs and winbar display funny symbols. How do I fix them? See + [this](#changing-the-default-signs) and [this](#customising-the-winbar) +9. What's this telemetry stuff all about? Are you sending my data to evil companies? + Debug adapters (for some reason) send telemetry data to clients. Vimspector simply + displays this information in the output window. It *does not* and *will not ever* + collect, use, forward or otherwise share any data with any third parties. +10. Do I _have_ to put a `.vimspector.json` in the root of every project? No, you + can put all of your adapter and debug configs in a [single directory](https://puremourning.github.io/vimspector/configuration.html#debug-configurations) if you want to, but note + the caveat that `${workspaceRoot}` won't be calculated correctly in that case. + The vimsepctor author uses this [a lot](https://github.com/puremourning/.vim-mac/tree/master/vimspector-conf). -Copyright © 2018 Ben Jackson [ycmd]: https://github.com/Valloric/ycmd [gitter]: https://gitter.im/vimspector/Lobby?utm_source=share-link&utm_medium=link&utm_campaign=share-link @@ -697,3 +2061,11 @@ Copyright © 2018 Ben Jackson [website]: https://puremourning.github.io/vimspector-web/ [delve]: https://github.com/go-delve/delve [delve-install]: https://github.com/go-delve/delve/tree/master/Documentation/installation +[vimspector-ref]: https://puremourning.github.io/vimspector/configuration.html +[vimspector-ref-var]: https://puremourning.github.io/vimspector/configuration.html#replacements-and-variables +[vimspector-ref-exception]: https://puremourning.github.io/vimspector/configuration.html#exception-breakpoints +[vimspector-ref-config-selection]: https://puremourning.github.io/vimspector/configuration.html#configuration-selection +[debugpy]: https://github.com/microsoft/debugpy +[YouCompleteMe]: https://github.com/ycm-core/YouCompleteMe#java-semantic-completion +[remote-debugging]: https://puremourning.github.io/vimspector/configuration.html#remote-debugging-support +[YcmJava]: https://github.com/ycm-core/YouCompleteMe#java-semantic-completion diff --git a/autoload/vimspector.vim b/autoload/vimspector.vim index 0250b90..78c7c1b 100644 --- a/autoload/vimspector.vim +++ b/autoload/vimspector.vim @@ -13,103 +13,559 @@ " See the License for the specific language governing permissions and " limitations under the License. +if !has( 'python3' ) + finish +endif " Boilerplate {{{ let s:save_cpo = &cpoptions set cpoptions&vim " }}} +function! s:Debug( ... ) abort + py3 < s:latest_completion_request.start_pos + " fix up the text (insert anything that is already present in the line + " that would be erased by the fixed-up earlier start position) + " + " both start_pos and item.start are 1-based + let item.text = s:latest_completion_request.line[ + \ s:latest_completion_request.start_pos + pfxlen - 1 : + \ item.start + pfxlen - 1 ] . item.text + endif + + if item.length > len( a:query ) + " call s:Debug( 'Rejecting %s, length is greater than %s', + " \ item, + " \ len( a:query ) ) + continue + endif + + call add( items, { 'word': item.text, + \ 'abbr': item.label, + \ 'menu': get( item, 'type', '' ), + \ 'icase': 1, + \ } ) + endfor + let s:latest_completion_request = {} + + " call s:Debug( 'Items: %s', items ) + return { 'words': items, 'refresh': 'always' } + endif +endfunction + +function! vimspector#OmniFuncWatch( find_start, query ) abort + return vimspector#CompleteFuncSync( 'Expression: ', a:find_start, a:query ) +endfunction + +function! vimspector#OmniFuncConsole( find_start, query ) abort + return vimspector#CompleteFuncSync( '> ', a:find_start, a:query ) +endfunction + +function! vimspector#Install( bang, ... ) abort + if !s:Enabled() + return + endif + let prefix = vimspector#internal#state#GetAPIPrefix() + py3 __import__( 'vimspector', + \ fromlist = [ 'installer' ] ).installer.RunInstaller( + \ vim.eval( 'prefix' ), + \ vim.eval( 'a:bang' ) == '!', + \ *vim.eval( 'a:000' ) ) +endfunction + +function! vimspector#CompleteInstall( ArgLead, CmdLine, CursorPos ) abort + if !s:Enabled() + return + endif + return py3eval( '"\n".join(' + \ . '__import__( "vimspector", fromlist = [ "gadgets" ] )' + \ . '.gadgets.GADGETS.keys() ' + \ . ')' ) +endfunction + +function! vimspector#Update( bang, ... ) abort + if !s:Enabled() + return + endif + + let prefix = vimspector#internal#state#GetAPIPrefix() + py3 __import__( 'vimspector', + \ fromlist = [ 'installer' ] ).installer.RunUpdate( + \ vim.eval( 'prefix' ), + \ vim.eval( 'a:bang' ) == '!', + \ *vim.eval( 'a:000' ) ) +endfunction + +function! vimspector#AbortInstall() abort + if !s:Enabled() + return + endif + + let prefix = vimspector#internal#state#GetAPIPrefix() + py3 __import__( 'vimspector', fromlist = [ 'installer' ] ).installer.Abort() +endfunction + + +function! vimspector#OnBufferCreated( file_name ) abort + if len( a:file_name ) == 0 + return + endif + + " Don't actually load up vimsepctor python in autocommands that trigger + " regularly. We'll only create the session obkect in s:Enabled() + if !s:Initialised() + return + endif + + if !s:Enabled() + return + endif + + py3 _vimspector_session.RefreshSigns( vim.eval( 'a:file_name' ) ) +endfunction + +function! vimspector#ShowEvalBalloon( is_visual ) abort + if a:is_visual + let expr = py3eval( '__import__( "vimspector", fromlist = [ "utils" ] )' + \ . '.utils.GetVisualSelection(' + \ . ' int( vim.eval( "winbufnr( winnr() )" ) ) )' ) + let expr = join( expr, '\n' ) + else + let expr = expand( '' ) + endif + + return py3eval( '_vimspector_session.ShowEvalBalloon(' + \ . ' int( vim.eval( "winnr()" ) ), "' + \ . expr + \ . '", 0 )' ) +endfunction + +function! vimspector#PrintDebugInfo() abort + if !s:Enabled() + return + endif + + py3 _vimspector_session.PrintDebugInfo() +endfunction + + " Boilerplate {{{ let &cpoptions=s:save_cpo unlet s:save_cpo diff --git a/autoload/vimspector/internal/balloon.vim b/autoload/vimspector/internal/balloon.vim index 6ae65b4..b607e51 100644 --- a/autoload/vimspector/internal/balloon.vim +++ b/autoload/vimspector/internal/balloon.vim @@ -19,15 +19,323 @@ let s:save_cpo = &cpoptions set cpoptions&vim " }}} -function! vimspector#internal#balloon#BalloonExpr() abort - " winnr + 1 because for *no good reason* winnr is 0 based here unlike - " everywhere else - " int() because for *no good reason* winnr is a string. - py3 _vimspector_session.ShowBalloon( int( vim.eval( 'v:beval_winnr' ) ) + 1, - \ vim.eval( 'v:beval_text' ) ) - return '...' +scriptencoding utf-8 + +let s:popup_win_id = 0 +let s:nvim_border_win_id = 0 +" +" tooltip dimensions +let s:min_width = 1 +let s:min_height = 1 +let s:max_width = 80 +let s:max_height = 20 + +let s:is_neovim = has( 'nvim' ) + + +" This is used as the balloonexpr in vim to show the Tooltip at the hover +" position +function! vimspector#internal#balloon#HoverTooltip() abort + return py3eval( '_vimspector_session.ShowEvalBalloon(' + \ . ' int( vim.eval( "v:beval_winnr" ) ) + 1,' + \ . ' vim.eval( "v:beval_text"),' + \ . ' 1 )' ) endfunction +function! vimspector#internal#balloon#CreateTooltip( is_hover, ... ) abort + let body = [] + if a:0 > 0 + let body = a:1 + endif + + if s:popup_win_id != 0 + call vimspector#internal#balloon#Close() + endif + + if s:is_neovim + call s:CreateNeovimTooltip( body ) + else + let config = { + \ 'wrap': 0, + \ 'filtermode': 'n', + \ 'maxwidth': s:max_width, + \ 'maxheight': s:max_height, + \ 'minwidth': s:min_width, + \ 'minheight': s:min_height, + \ 'scrollbar': 1, + \ 'border': [], + \ 'padding': [ 0, 1, 0, 1], + \ 'drag': 1, + \ 'resize': 1, + \ 'close': 'button', + \ 'callback': 'vimspector#internal#balloon#CloseCallback', + \ } + + let config = vimspector#internal#popup#SetBorderChars( config ) + + if a:is_hover + let config[ 'filter' ] = 'vimspector#internal#balloon#MouseFilter' + let config[ 'mousemoved' ] = [ 0, 0, 0 ] + let s:popup_win_id = popup_beval( body, config ) + else + let config[ 'filter' ] = 'vimspector#internal#balloon#CursorFilter' + let config[ 'moved' ] = 'any' + let config[ 'cursorline' ] = 1 + let config[ 'mapping' ] = 0 + let s:popup_win_id = popup_atcursor( body, config ) + endif + + endif + + return s:popup_win_id +endfunction + +" Filters for vim {{{ +function! vimspector#internal#balloon#MouseFilter( winid, key ) abort + if a:key ==# "\" + call vimspector#internal#balloon#Close() + return 0 + endif + + if index( [ "\", "\<2-leftmouse>" ], a:key ) < 0 + return 0 + endif + + let handled = 0 + let mouse_coords = getmousepos() + + " close the popup if mouse is clicked outside the window + if mouse_coords[ 'winid' ] != a:winid + call vimspector#internal#balloon#Close() + return 0 + endif + + " place the cursor according to the click + call win_execute( a:winid, + \ ':call cursor( ' + \ . mouse_coords[ 'line' ] + \ . ', ' + \ . mouse_coords[ 'column' ] + \ . ' )' ) + + " expand the variable if we got double click + if a:key ==? "\<2-leftmouse>" + call py3eval( '_vimspector_session.ExpandVariable(' + \ . 'buf = vim.buffers[ ' . winbufnr( a:winid ) . ' ],' + \ . 'line_num = ' . line( '.', a:winid ) + \ . ')' ) + let handled = 1 + endif + + return handled +endfunction + +function! s:MatchKey( key, candidates ) abort + for candidate in a:candidates + " If the mapping string looks like a special character, then try and + " expand it. This is... a hack. The whole thing only works if the mapping + " is a single key (anyway), and so we assume any string starting with < is a + " special key (which will be the common case) and try and map it. If it + " fails... it fails. + if candidate[ 0 ] == '<' + try + execute 'let candidate = "\' . candidate . '"' + endtry + endif + + if candidate ==# a:key + return v:true + endif + endfor + + return v:false +endfunction + +function! vimspector#internal#balloon#CursorFilter( winid, key ) abort + let mappings = py3eval( + \ "__import__( 'vimspector'," + \." fromlist = [ 'settings' ] ).settings.Dict(" + \." 'mappings' )[ 'variables' ]" ) + + if index( [ "\", "\<2-LeftMouse>" ], a:key ) >= 0 + return vimspector#internal#balloon#MouseFilter( a:winid, a:key ) + endif + + if s:MatchKey( a:key, mappings.expand_collapse ) + call py3eval( '_vimspector_session.ExpandVariable(' + \ . 'buf = vim.buffers[ ' . winbufnr( a:winid ) . ' ],' + \ . 'line_num = ' . line( '.', a:winid ) + \ . ')' ) + return 1 + elseif s:MatchKey( a:key, mappings.set_value ) + call py3eval( '_vimspector_session.SetVariableValue(' + \ . 'buf = vim.buffers[ ' . winbufnr( a:winid ) . ' ],' + \ . 'line_num = ' . line( '.', a:winid ) + \ . ')' ) + return 1 + endif + + return popup_filter_menu( a:winid, a:key ) +endfunction + +" }}} + +" Closing {{{ + +function! vimspector#internal#balloon#CloseCallback( ... ) abort + let s:popup_win_id = 0 + let s:nvim_border_win_id = 0 + return py3eval( '_vimspector_session.CleanUpTooltip()' ) +endfunction + +function! vimspector#internal#balloon#Close() abort + if s:popup_win_id == 0 + return + endif + + if s:is_neovim + call nvim_win_close( s:popup_win_id, v:true ) + call nvim_win_close( s:nvim_border_win_id, v:true ) + + call vimspector#internal#balloon#CloseCallback() + else + call popup_close(s:popup_win_id) + endif +endfunction + +" }}} + +" Neovim pollyfill {{{ + +function! vimspector#internal#balloon#ResizeTooltip() abort + if !s:is_neovim + " Vim does this for us + return + endif + + if s:popup_win_id <= 0 || s:nvim_border_win_id <= 0 + " nothing to resize + return + endif + + noautocmd call win_gotoid( s:popup_win_id ) + let buf_lines = getline( 1, '$' ) + + let width = s:min_width + let height = min( [ max( [ s:min_height, len( buf_lines ) ] ), + \ s:max_height ] ) + + " calculate the longest line + for l in buf_lines + let width = max( [ width, len( l ) ] ) + endfor + + let width = min( [ width, s:max_width ] ) + + let opts = { + \ 'width': width, + \ 'height': height, + \ } + + " resize the content window + call nvim_win_set_config( s:popup_win_id, opts ) + + " resize the border window + let opts[ 'width' ] = width + 4 + let opts[ 'height' ] = height + 2 + + call nvim_win_set_config( s:nvim_border_win_id, opts ) + call nvim_buf_set_lines( nvim_win_get_buf( s:nvim_border_win_id ), + \ 0, + \ -1, + \ v:true, + \ s:GenerateBorder( width, height ) ) +endfunction + +" neovim doesn't have the border support, so we have to make our own. +" FIXME: This will likely break if the user has `ambiwidth=2` +function! s:GenerateBorder( width, height ) abort + + let top = '╭' . repeat('─',a:width + 2) . '╮' + let mid = '│' . repeat(' ',a:width + 2) . '│' + let bot = '╰' . repeat('─',a:width + 2) . '╯' + let lines = [ top ] + repeat( [ mid ], a:height ) + [ bot ] + + return lines +endfunction + +function! s:CreateNeovimTooltip( body ) abort + " generate border for the float window by creating a background buffer and + " overlaying the content buffer + " see https://github.com/neovim/neovim/issues/9718#issuecomment-546603628 + let buf_id = nvim_create_buf( v:false, v:true ) + call nvim_buf_set_lines( buf_id, + \ 0, + \ -1, + \ v:true, + \ s:GenerateBorder( s:max_width, s:max_height ) ) + + " default the dimensions initially, then we'll calculate the real size and + " resize it. + let opts = { + \ 'relative': 'cursor', + \ 'width': s:max_width + 2, + \ 'height': s:max_height + 2, + \ 'col': 0, + \ 'row': 1, + \ 'anchor': 'NW', + \ 'style': 'minimal' + \ } + + " this is the border window + let s:nvim_border_win_id = nvim_open_win( buf_id, 0, opts ) + call nvim_win_set_option( s:nvim_border_win_id, 'signcolumn', 'no' ) + call nvim_win_set_option( s:nvim_border_win_id, 'relativenumber', v:false ) + call nvim_win_set_option( s:nvim_border_win_id, 'number', v:false ) + + " when calculating where to display the content window, we need to account + " for the border + let opts.row += 1 + let opts.height -= 2 + let opts.col += 2 + let opts.width -= 4 + + " create the content window + let buf_id = nvim_create_buf( v:false, v:true ) + call nvim_buf_set_lines( buf_id, 0, -1, v:true, a:body ) + call nvim_buf_set_option( buf_id, 'modifiable', v:false ) + let s:popup_win_id = nvim_open_win( buf_id, v:false, opts ) + + " Apparently none of these work, when 'style' is 'minimal' + call nvim_win_set_option( s:popup_win_id, 'wrap', v:false ) + call nvim_win_set_option( s:popup_win_id, 'cursorline', v:true ) + call nvim_win_set_option( s:popup_win_id, 'signcolumn', 'no' ) + call nvim_win_set_option( s:popup_win_id, 'relativenumber', v:false ) + call nvim_win_set_option( s:popup_win_id, 'number', v:false ) + + " Move the cursor into the popup window, as this is the only way we can + " interract with the popup in neovim + noautocmd call win_gotoid( s:popup_win_id ) + + nnoremap quit + call py3eval( "__import__( 'vimspector', " + \." fromlist = [ 'variables' ] )." + \.' variables.AddExpandMappings()' ) + + " Close the popup whenever we leave this window + augroup vimspector#internal#balloon#nvim_float + autocmd! + autocmd WinLeave + \ :call vimspector#internal#balloon#Close() + \ | autocmd! vimspector#internal#balloon#nvim_float + augroup END + + call vimspector#internal#balloon#ResizeTooltip() +endfunction + +" }}} + + " Boilerplate {{{ let &cpoptions=s:save_cpo unlet s:save_cpo diff --git a/autoload/vimspector/internal/channel.vim b/autoload/vimspector/internal/channel.vim index d0cb7d2..e033cff 100644 --- a/autoload/vimspector/internal/channel.vim +++ b/autoload/vimspector/internal/channel.vim @@ -20,42 +20,49 @@ set cpoptions&vim " }}} function! s:_OnServerData( channel, data ) abort + if !exists( 's:ch' ) || s:ch isnot a:channel + return + endif + py3 << EOF _vimspector_session.OnChannelData( vim.eval( 'a:data' ) ) EOF endfunction -function! s:_OnServerError( channel, data ) abort - echom 'Channel received error: ' . a:data - redraw -endfunction - function! s:_OnClose( channel ) abort + if !exists( 's:ch' ) || s:ch isnot a:channel + return + endif + echom 'Channel closed' redraw unlet s:ch py3 _vimspector_session.OnServerExit( 0 ) endfunction -function! s:_Send( msg ) abort - call ch_sendraw( s:ch, a:msg ) - return 1 -endfunction - -function! vimspector#internal#channel#Timeout( id ) abort - py3 << EOF -_vimspector_session.OnRequestTimeout( vim.eval( 'a:id' ) ) -EOF -endfunction - function! vimspector#internal#channel#StartDebugSession( config ) abort if exists( 's:ch' ) echo 'Channel is already running' - return v:none + return v:false endif - let l:addr = 'localhost:' . a:config[ 'port' ] + " If we _also_ have a command line, then start the actual job. This allows for + " servers which start up and listen on some port + if has_key( a:config, 'command' ) + let s:job = job_start( a:config[ 'command' ], + \ { + \ 'in_mode': 'raw', + \ 'out_mode': 'raw', + \ 'err_mode': 'raw', + \ 'stoponexit': 'term', + \ 'env': a:config[ 'env' ], + \ 'cwd': a:config[ 'cwd' ], + \ } + \ ) + endif + + let l:addr = get( a:config, 'host', '127.0.0.1' ) . ':' . a:config[ 'port' ] echo 'Connecting to ' . l:addr . '... (waiting fo up to 10 seconds)' let s:ch = ch_open( l:addr, @@ -68,44 +75,72 @@ function! vimspector#internal#channel#StartDebugSession( config ) abort \ ) if ch_status( s:ch ) !=# 'open' - echom 'Unable to connect to debug adapter' + echom 'Unable to connect to' l:addr redraw - return v:none + return v:false endif - return funcref( 's:_Send' ) + return v:true +endfunction + +function! vimspector#internal#channel#Send( msg ) abort + call ch_sendraw( s:ch, a:msg ) + return 1 +endfunction + +function! vimspector#internal#channel#Timeout( id ) abort + py3 << EOF +_vimspector_session.OnRequestTimeout( vim.eval( 'a:id' ) ) +EOF endfunction function! vimspector#internal#channel#StopDebugSession() abort - if !exists( 's:ch' ) - return - endif - if ch_status( s:ch ) ==# 'open' + if exists( 's:job' ) + " We started the job, so we need to kill it and wait to read all the data + " from the socket + + if job_status( s:job ) ==# 'run' + call job_stop( s:job, 'term' ) + endif + + while job_status( s:job ) ==# 'run' + call job_stop( s:job, 'kill' ) + endwhile + + unlet s:job + + if exists( 's:ch' ) && count( [ 'closed', 'fail' ], ch_status( s:ch ) ) == 0 + " We're going to block on this channel reading, then manually call the + " close callback, so remove the automatic close callback to avoid tricky + " re-entrancy + call ch_setoptions( s:ch, { 'close_cb': '' } ) + endif + + elseif exists( 's:ch' ) && + \ count( [ 'closed', 'fail' ], ch_status( s:ch ) ) == 0 + " channel is open, close it and trigger the callback. The callback is _not_ " triggered when manually calling ch_close. if we get here and the channel " is not open, then we there is a _OnClose callback waiting for us, so do " nothing. call ch_close( s:ch ) - call s:_OnClose( s:ch ) endif + + " block until we've read all data from the socket and handled it. + while count( [ 'open', 'buffered' ], ch_status( s:ch ) ) == 1 + let data = ch_read( s:ch, { 'timeout': 10 } ) + call s:_OnServerData( s:ch, data ) + endwhile + call s:_OnClose( s:ch ) endfunction function! vimspector#internal#channel#Reset() abort - if exists( 's:ch' ) + if exists( 's:ch' ) || exists( 's:job' ) call vimspector#internal#channel#StopDebugSession() endif endfunction -function! vimspector#internal#channel#ForceRead() abort - if exists( 's:ch' ) - let data = ch_readraw( s:ch, { 'timeout': 1000 } ) - if data !=# '' - call s:_OnServerData( s:ch, data ) - endif - endif -endfunction - " Boilerplate {{{ let &cpoptions=s:save_cpo unlet s:save_cpo diff --git a/autoload/vimspector/internal/job.vim b/autoload/vimspector/internal/job.vim index 26ba117..206baf0 100644 --- a/autoload/vimspector/internal/job.vim +++ b/autoload/vimspector/internal/job.vim @@ -20,26 +20,91 @@ set cpoptions&vim " }}} function! s:_OnServerData( channel, data ) abort + if !exists( 's:job' ) || ch_getjob( a:channel ) isnot s:job + call ch_log( 'Get data after process exit' ) + return + endif + py3 _vimspector_session.OnChannelData( vim.eval( 'a:data' ) ) endfunction function! s:_OnServerError( channel, data ) abort + if !exists( 's:job' ) || ch_getjob( a:channel ) isnot s:job + call ch_log( 'Get data after process exit' ) + return + endif + py3 _vimspector_session.OnServerStderr( vim.eval( 'a:data' ) ) endfunction + +" FIXME: We should wait until both the exit_cb _and_ the channel closed callback +" have been received before OnServerExit? + function! s:_OnExit( channel, status ) abort + if !exists( 's:job' ) || ch_getjob( a:channel ) isnot s:job + call ch_log( 'Unexpected exit callback' ) + return + endif + echom 'Channel exit with status ' . a:status redraw - unlet s:job + if exists( 's:job' ) + unlet s:job + endif py3 _vimspector_session.OnServerExit( vim.eval( 'a:status' ) ) endfunction function! s:_OnClose( channel ) abort + if !exists( 's:job' ) || job_getchannel( s:job ) != a:channel + call ch_log( 'Channel closed after exit' ) + return + endif + echom 'Channel closed' redraw endfunction -function! s:_Send( msg ) abort +function! vimspector#internal#job#StartDebugSession( config ) abort + if exists( 's:job' ) + echom 'Not starting: Job is already running' + redraw + return v:false + endif + + let s:job = job_start( a:config[ 'command' ], + \ { + \ 'in_mode': 'raw', + \ 'out_mode': 'raw', + \ 'err_mode': 'raw', + \ 'exit_cb': funcref( 's:_OnExit' ), + \ 'close_cb': funcref( 's:_OnClose' ), + \ 'out_cb': funcref( 's:_OnServerData' ), + \ 'err_cb': funcref( 's:_OnServerError' ), + \ 'stoponexit': 'term', + \ 'env': a:config[ 'env' ], + \ 'cwd': a:config[ 'cwd' ], + \ } + \ ) + + if !exists( 's:job' ) + " The job died immediately after starting and we cleaned up + return v:false + endif + + let status = job_status( s:job ) + + echom 'Started job, status is: ' . status + redraw + + if status !=# 'run' + return v:false + endif + + return v:true +endfunction + +function! vimspector#internal#job#Send( msg ) abort if ! exists( 's:job' ) echom "Can't send message: Job was not initialised correctly" redraw @@ -63,40 +128,6 @@ function! s:_Send( msg ) abort return 1 endfunction -function! vimspector#internal#job#StartDebugSession( config ) abort - if exists( 's:job' ) - echom 'Not starging: Job is already running' - redraw - return v:none - endif - - let s:job = job_start( a:config[ 'command' ], - \ { - \ 'in_mode': 'raw', - \ 'out_mode': 'raw', - \ 'err_mode': 'raw', - \ 'exit_cb': funcref( 's:_OnExit' ), - \ 'close_cb': funcref( 's:_OnClose' ), - \ 'out_cb': funcref( 's:_OnServerData' ), - \ 'err_cb': funcref( 's:_OnServerError' ), - \ 'stoponexit': 'term', - \ 'env': a:config[ 'env' ], - \ 'cwd': a:config[ 'cwd' ], - \ } - \ ) - - echom 'Started job, status is: ' . job_status( s:job ) - redraw - - if job_status( s:job ) !=# 'run' - echom 'Unable to start job, status is: ' . job_status( s:job ) - redraw - return v:none - endif - - return funcref( 's:_Send' ) -endfunction - function! vimspector#internal#job#StopDebugSession() abort if !exists( 's:job' ) echom "Not stopping session: Job doesn't exist" @@ -105,8 +136,8 @@ function! vimspector#internal#job#StopDebugSession() abort endif if job_status( s:job ) ==# 'run' - echom 'Terminating job' - redraw + echom 'Terminating job' + redraw call job_stop( s:job, 'kill' ) endif endfunction @@ -115,13 +146,11 @@ function! vimspector#internal#job#Reset() abort call vimspector#internal#job#StopDebugSession() endfunction -function! vimspector#internal#job#ForceRead() abort - if exists( 's:job' ) - let data = ch_readraw( job_getchannel( s:job ), { 'timeout': 1000 } ) - if data !=# '' - call s:_OnServerData( job_getchannel( s:job ), data ) - endif - endif +function! s:_OnCommandExit( category, ch, code ) abort + py3 __import__( "vimspector", + \ fromlist = [ "utils" ] ).utils.OnCommandWithLogComplete( + \ vim.eval( 'a:category' ), + \ int( vim.eval( 'a:code' ) ) ) endfunction function! vimspector#internal#job#StartCommandWithLog( cmd, category ) abort @@ -135,31 +164,30 @@ function! vimspector#internal#job#StartCommandWithLog( cmd, category ) abort let l:index = len( s:commands[ a:category ] ) + let buf = '_vimspector_log_' . a:category + call add( s:commands[ a:category ], job_start( - \ a:cmd, + \ a:cmd, \ { \ 'out_io': 'buffer', - \ 'in_io': 'null', \ 'err_io': 'buffer', - \ 'out_name': '_vimspector_log_' . a:category . '_out', - \ 'err_name': '_vimspector_log_' . a:category . '_err', + \ 'out_msg': 0, + \ 'err_msg': 0, + \ 'out_name': buf, + \ 'err_name': buf, + \ 'exit_cb': funcref( 's:_OnCommandExit', [ a:category ] ), \ 'out_modifiable': 0, \ 'err_modifiable': 0, \ 'stoponexit': 'kill' \ } ) ) if job_status( s:commands[ a:category ][ index ] ) !=# 'run' - echom 'Unable to start job for ' . a:cmd + echom 'Unable to start job for ' . string( a:cmd ) redraw return v:none endif - let l:stdout = ch_getbufnr( - \ job_getchannel( s:commands[ a:category ][ index ] ), 'out' ) - let l:stderr = ch_getbufnr( - \ job_getchannel( s:commands[ a:category ][ index ] ), 'err' ) - - return [ l:stdout, l:stderr ] + return bufnr( buf ) endfunction diff --git a/autoload/vimspector/internal/neochannel.vim b/autoload/vimspector/internal/neochannel.vim new file mode 100644 index 0000000..f20684d --- /dev/null +++ b/autoload/vimspector/internal/neochannel.vim @@ -0,0 +1,126 @@ +" vimspector - A multi-language debugging system for Vim +" Copyright 2020 Ben Jackson +" +" Licensed under the Apache License, Version 2.0 (the "License"); +" you may not use this file except in compliance with the License. +" You may obtain a copy of the License at +" +" http://www.apache.org/licenses/LICENSE-2.0 +" +" Unless required by applicable law or agreed to in writing, software +" distributed under the License is distributed on an "AS IS" BASIS, +" WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +" See the License for the specific language governing permissions and +" limitations under the License. + + +" Boilerplate {{{ +let s:save_cpo = &cpoptions +set cpoptions&vim +" }}} + + + +function! s:_OnEvent( chan_id, data, event ) abort + if v:exiting isnot# v:null + return + endif + + if !exists( 's:ch' ) || a:chan_id != s:ch + return + endif + + if a:data == [''] + echom 'Channel closed' + redraw + unlet s:ch + py3 _vimspector_session.OnServerExit( 0 ) + else + py3 _vimspector_session.OnChannelData( '\n'.join( vim.eval( 'a:data' ) ) ) + endif +endfunction + +function! vimspector#internal#neochannel#StartDebugSession( config ) abort + if exists( 's:ch' ) + echom 'Not starting: Channel is already running' + redraw + return v:false + endif + + " If we _also_ have a command line, then start the actual job. This allows for + " servers which start up and listen on some port + if has_key( a:config, 'command' ) + let old_env={} + try + let old_env = vimspector#internal#neoterm#PrepareEnvironment( + \ a:config[ 'env' ] ) + let s:job = jobstart( a:config[ 'command' ], + \ { + \ 'cwd': a:config[ 'cwd' ], + \ 'env': a:config[ 'env' ], + \ } + \ ) + finally + call vimspector#internal#neoterm#ResetEnvironment( a:config[ 'env' ], + \ old_env ) + endtry + endif + + let l:addr = get( a:config, 'host', '127.0.0.1' ) . ':' . a:config[ 'port' ] + + let attempt = 1 + while attempt <= 10 + echo 'Connecting to ' . l:addr . '... (attempt' attempt 'of 10)' + try + let s:ch = sockconnect( 'tcp', + \ addr, + \ { 'on_data': funcref( 's:_OnEvent' ) } ) + redraw + return v:true + catch /connection refused/ + sleep 1 + endtry + let attempt += 1 + endwhile + + echom 'Unable to connect to' l:addr 'after 10 attempts' + redraw + return v:false +endfunction + +function! vimspector#internal#neochannel#Send( msg ) abort + if ! exists( 's:ch' ) + echom "Can't send message: Channel was not initialised correctly" + redraw + return 0 + endif + + call chansend( s:ch, a:msg ) + return 1 +endfunction + +function! vimspector#internal#neochannel#StopDebugSession() abort + if exists( 's:ch' ) + call chanclose( s:ch ) + " It doesn't look like we get a callback after chanclos. Who knows if we + " will subsequently receive data callbacks. + call s:_OnEvent( s:ch, [ '' ], 'data' ) + endif + + if exists( 's:job' ) + if vimspector#internal#neojob#JobIsRunning( s:job ) + call jobstop( s:job ) + endif + unlet s:job + endif +endfunction + +function! vimspector#internal#neochannel#Reset() abort + call vimspector#internal#neochannel#StopDebugSession() +endfunction + +" Boilerplate {{{ +let &cpoptions=s:save_cpo +unlet s:save_cpo +" }}} + diff --git a/autoload/vimspector/internal/neojob.vim b/autoload/vimspector/internal/neojob.vim new file mode 100644 index 0000000..a398afe --- /dev/null +++ b/autoload/vimspector/internal/neojob.vim @@ -0,0 +1,252 @@ +" vimspector - A multi-language debugging system for Vim +" Copyright 2020 Ben Jackson +" +" Licensed under the Apache License, Version 2.0 (the "License"); +" you may not use this file except in compliance with the License. +" You may obtain a copy of the License at +" +" http://www.apache.org/licenses/LICENSE-2.0 +" +" Unless required by applicable law or agreed to in writing, software +" distributed under the License is distributed on an "AS IS" BASIS, +" WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +" See the License for the specific language governing permissions and +" limitations under the License. + + +" Boilerplate {{{ +let s:save_cpo = &cpoptions +set cpoptions&vim +" }}} + + + +function! s:_OnEvent( chan_id, data, event ) abort + if v:exiting isnot# v:null + return + endif + + if !exists( 's:job' ) || a:chan_id != s:job + return + endif + + " In neovim, the data argument is a list. + if a:event ==# 'stdout' + py3 _vimspector_session.OnChannelData( '\n'.join( vim.eval( 'a:data' ) ) ) + elseif a:event ==# 'stderr' + py3 _vimspector_session.OnServerStderr( '\n'.join( vim.eval( 'a:data' ) ) ) + elseif a:event ==# 'exit' + echom 'Channel exit with status ' . a:data + redraw + unlet s:job + py3 _vimspector_session.OnServerExit( vim.eval( 'a:data' ) ) + endif +endfunction + +function! vimspector#internal#neojob#StartDebugSession( config ) abort + if exists( 's:job' ) + echom 'Not starging: Job is already running' + redraw + return v:false + endif + + + " HACK: Workaround for 'env' not being supported. + + let old_env={} + try + let old_env = vimspector#internal#neoterm#PrepareEnvironment( + \ a:config[ 'env' ] ) + let s:job = jobstart( a:config[ 'command' ], + \ { + \ 'on_stdout': funcref( 's:_OnEvent' ), + \ 'on_stderr': funcref( 's:_OnEvent' ), + \ 'on_exit': funcref( 's:_OnEvent' ), + \ 'cwd': a:config[ 'cwd' ], + \ 'env': a:config[ 'env' ], + \ } + \ ) + finally + call vimspector#internal#neoterm#ResetEnvironment( a:config[ 'env' ], + \ old_env ) + endtry + + return v:true +endfunction + +function! vimspector#internal#neojob#JobIsRunning( job ) abort + return jobwait( [ a:job ], 0 )[ 0 ] == -1 +endfunction + +function! vimspector#internal#neojob#Send( msg ) abort + if ! exists( 's:job' ) + echom "Can't send message: Job was not initialised correctly" + redraw + return 0 + endif + + if !vimspector#internal#neojob#JobIsRunning( s:job ) + echom "Can't send message: Job is not running" + redraw + return 0 + endif + + call chansend( s:job, a:msg ) + return 1 +endfunction + +function! vimspector#internal#neojob#StopDebugSession() abort + if !exists( 's:job' ) + return + endif + + if vimspector#internal#neojob#JobIsRunning( s:job ) + echom 'Terminating job' + redraw + call jobstop( s:job ) + endif +endfunction + +function! vimspector#internal#neojob#Reset() abort + call vimspector#internal#neojob#StopDebugSession() +endfunction + +function! s:_OnCommandEvent( category, id, data, event ) abort + if v:exiting isnot# v:null + return + endif + + if a:event ==# 'stdout' || a:event ==# 'stderr' + if a:data == [''] + return + endif + + if !has_key( s:commands, a:category ) + return + endif + + if !has_key( s:commands[ a:category ], a:id ) + return + endif + + if a:event ==# 'stdout' + let buffer = s:commands[ a:category ][ a:id ].stdout + elseif a:event ==# 'stderr' + let buffer = s:commands[ a:category ][ a:id ].stderr + endif + + try + call bufload( buffer ) + catch /E325/ + " Ignore E325/ATTENTION + endtry + + + let numlines = py3eval( "len( vim.buffers[ int( vim.eval( 'buffer' ) ) ] )" ) + let last_line = getbufline( buffer, '$' )[ 0 ] + + call s:MakeBufferWritable( buffer ) + try + if numlines == 1 && last_line ==# '' + call setbufline( buffer, 1, a:data[ 0 ] ) + else + call setbufline( buffer, '$', last_line . a:data[ 0 ] ) + endif + + call appendbufline( buffer, '$', a:data[ 1: ] ) + finally + call s:MakeBufferReadOnly( buffer ) + call setbufvar( buffer, '&modified', 0 ) + endtry + + " if the buffer is visible, scroll it, but don't allow autocommands to fire, + " as this may close the current window! + let w = bufwinnr( buffer ) + if w > 0 + let cw = winnr() + try + noautocmd execute w . 'wincmd w' + noautocmd normal! Gz- + finally + noautocmd execute cw . 'wincmd w' + endtry + endif + elseif a:event ==# 'exit' + py3 __import__( "vimspector", + \ fromlist = [ "utils" ] ).utils.OnCommandWithLogComplete( + \ vim.eval( 'a:category' ), + \ int( vim.eval( 'a:data' ) ) ) + endif +endfunction + +function! s:SetUpHiddenBuffer( buffer ) abort + call setbufvar( a:buffer, '&hidden', 1 ) + call setbufvar( a:buffer, '&bufhidden', 'hide' ) + call setbufvar( a:buffer, '&wrap', 0 ) + call setbufvar( a:buffer, '&swapfile', 0 ) + call setbufvar( a:buffer, '&textwidth', 0 ) + call s:MakeBufferReadOnly( a:buffer ) +endfunction + +function! s:MakeBufferReadOnly( buffer ) abort + call setbufvar( a:buffer, '&modifiable', 0 ) + call setbufvar( a:buffer, '&readonly', 1 ) +endfunction + +function! s:MakeBufferWritable( buffer ) abort + call setbufvar( a:buffer, '&readonly', 0 ) + call setbufvar( a:buffer, '&modifiable', 1 ) +endfunction + + +let s:commands = {} + +function! vimspector#internal#neojob#StartCommandWithLog( cmd, category ) abort + if ! has_key( s:commands, a:category ) + let s:commands[ a:category ] = {} + endif + + let buf = bufnr( '_vimspector_log_' . a:category, v:true ) + + " FIXME: This largely duplicates the same stuff in the python layer, but we + " don't want to potentially mess up Vim behaviour where the job output is + " attached to a buffer set up by Vim. So we sort o mimic that here. + call s:SetUpHiddenBuffer( buf ) + + let id = jobstart(a:cmd, + \ { + \ 'on_stdout': funcref( 's:_OnCommandEvent', + \ [ a:category ] ), + \ 'on_stderr': funcref( 's:_OnCommandEvent', + \ [ a:category ] ), + \ 'on_exit': funcref( 's:_OnCommandEvent', + \ [ a:category ] ), + \ } ) + + let s:commands[ a:category ][ id ] = { + \ 'stdout': buf, + \ 'stderr': buf + \ } + + return buf +endfunction + +function! vimspector#internal#neojob#CleanUpCommand( category ) abort + if ! has_key( s:commands, a:category ) + return + endif + + for id in keys( s:commands[ a:category ] ) + let id = str2nr( id ) + if jobwait( [ id ], 0 )[ 0 ] == -1 + call jobstop( id ) + endif + call jobwait( [ id ], -1 ) + endfor + unlet! s:commands[ a:category ] +endfunction + +" Boilerplate {{{ +let &cpoptions=s:save_cpo +unlet s:save_cpo +" }}} diff --git a/autoload/vimspector/internal/neopopup.vim b/autoload/vimspector/internal/neopopup.vim new file mode 100644 index 0000000..a734ca3 --- /dev/null +++ b/autoload/vimspector/internal/neopopup.vim @@ -0,0 +1,137 @@ +" vimspector - A multi-language debugging system for Vim +" Copyright 2018 Ben Jackson +" +" Licensed under the Apache License, Version 2.0 (the "License"); +" you may not use this file except in compliance with the License. +" You may obtain a copy of the License at +" +" http://www.apache.org/licenses/LICENSE-2.0 +" +" Unless required by applicable law or agreed to in writing, software +" distributed under the License is distributed on an "AS IS" BASIS, +" WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +" See the License for the specific language governing permissions and +" limitations under the License. + +" Boilerplate {{{ +let s:save_cpo = &cpoptions +set cpoptions&vim +" }}} + +" Neovim's float window API, like its job/channel API is painful to use +" compared to Vim's so we have to employ more hacks + +" We can't seem to pass a Window handle back to the python, so we have to +" maintain yet another cached here +let s:db = {} +let s:next_id = 0 + + +function! s:MessageToList( message ) abort + if type( a:message ) == type( [] ) + let message = a:message + else + let message = [ a:message ] + endif + return message +endfunction + +function! s:GetSplashConfig( message ) abort + let l = max( map( a:message, 'len( v:val )' ) ) + let h = len( a:message ) + + return { 'relative': 'editor', + \ 'width': l, + \ 'height': h, + \ 'col': ( &columns / 2 ) - ( l / 2 ), + \ 'row': ( &lines / 2 ) - h / 2, + \ 'anchor': 'NW', + \ 'style': 'minimal', + \ 'focusable': v:false, + \ } +endfunction + +function! vimspector#internal#neopopup#DisplaySplash( message ) abort + let message = s:MessageToList( a:message ) + let buf = nvim_create_buf(v:false, v:true) + call nvim_buf_set_lines(buf, 0, -1, v:true, message ) + + let win = nvim_open_win(buf, 0, s:GetSplashConfig( message ) ) + call nvim_win_set_option(win, 'wrap', v:false) + call nvim_win_set_option(win, 'colorcolumn', '') + + let id = s:next_id + let s:next_id += 1 + let s:db[ id ] = { 'win': win, 'buf': buf } + return id +endfunction + +function! vimspector#internal#neopopup#UpdateSplash( id, message ) abort + let splash = s:db[ a:id ] + let message = s:MessageToList( a:message ) + call nvim_buf_set_lines( splash.buf, 0, -1, v:true, message ) + call nvim_win_set_config( splash.win, s:GetSplashConfig( message ) ) + return a:id +endfunction + +function! vimspector#internal#neopopup#HideSplash( id ) abort + let splash = s:db[ a:id ] + call nvim_win_close( splash.win, v:true ) + unlet s:db[ a:id ] +endfunction + +function! vimspector#internal#neopopup#Confirm( confirm_id, + \ text, + \ options, + \ default_value, + \ keys ) abort + + " Neovim doesn't have an equivalent of popup_dialog, and it's way too much + " effort to write one, so we just use confirm()... + " Annoyingly we can't use confirm() here because for some reason it doesn't + " render properly in a channel callback. So we use input() and mimic dialog + " behaviour. + let prompt = a:text + for opt in a:options + let prompt .= ' ' . opt + endfor + let prompt .= ': ' + + try + let result = input( prompt, a:keys[ a:default_value - 1 ] ) + catch /.*/ + let result = -1 + endtry + + " Map the results to what the vim popup stuff would return (s:ConfirmCallback + " in popup.vim), i.e.: + " - 1-based index of selected item, or + " - -1 or 0 for cancellation + if result == '' + " User pressed ESC/ctrl-c + let result = -1 + else + let index = 1 + for k in a:keys + if k ==? result + let result = index + break + endif + let index += 1 + endfor + + if index > len( a:keys ) + let result = -1 + endif + endif + + py3 __import__( 'vimspector', fromlist = [ 'utils' ] ).utils.ConfirmCallback( + \ int( vim.eval( 'a:confirm_id' ) ), + \ int( vim.eval( 'result' ) ) ) +endfunction + +" Boilerplate {{{ +let &cpoptions=s:save_cpo +unlet s:save_cpo +" }}} + diff --git a/autoload/vimspector/internal/neoterm.vim b/autoload/vimspector/internal/neoterm.vim new file mode 100644 index 0000000..3f5bf66 --- /dev/null +++ b/autoload/vimspector/internal/neoterm.vim @@ -0,0 +1,106 @@ +" vimspector - A multi-language debugging system for Vim +" Copyright 2018 Ben Jackson +" +" Licensed under the Apache License, Version 2.0 (the "License"); +" you may not use this file except in compliance with the License. +" You may obtain a copy of the License at +" +" http://www.apache.org/licenses/LICENSE-2.0 +" +" Unless required by applicable law or agreed to in writing, software +" distributed under the License is distributed on an "AS IS" BASIS, +" WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +" See the License for the specific language governing permissions and +" limitations under the License. + + +" Boilerplate {{{ +let s:save_cpo = &cpoptions +set cpoptions&vim +" }}} + +" Ids are unique throughtout the life of neovim, but obviously buffer numbers +" aren't +" +" FIXME: Tidy this map when buffers are closed ? +let s:buffer_to_id = {} + +function! vimspector#internal#neoterm#PrepareEnvironment( env ) abort + let old_env = {} + + for key in keys( a:env ) + if exists( '$' . key ) + let old_env[ key ] = getenv( key ) + endif + call setenv( key, a:env[ key ] ) + endfor + + return old_env +endfunction + +function! vimspector#internal#neoterm#ResetEnvironment( env, old_env ) abort + for key in keys( a:env ) + let value = get( a:old_env, key, v:null ) + call setenv( key, value ) + endfor +endfunction + +function! vimspector#internal#neoterm#Start( cmd, opts ) abort + " Prepare current buffer to be turned into a term if curwin is not set + if ! get( a:opts, 'curwin', 0 ) + let mods = 'rightbelow ' + if get( a:opts, 'vertical', 0 ) + let mods .= 'vertical ' + let mods .= get( a:opts, 'term_cols', '' ) + else + let mods .= get( a:opts, 'term_rows', '' ) + endif + + execute mods . 'new' + endif + + " HACK: Neovim's termopen doesn't support env + + let old_env={} + try + let old_env = vimspector#internal#neoterm#PrepareEnvironment( + \ a:opts[ 'env' ] ) + setlocal nomodified + let id = termopen( a:cmd, { + \ 'cwd': a:opts[ 'cwd' ], + \ 'env': a:opts[ 'env' ], + \ } ) + finally + call vimspector#internal#neoterm#ResetEnvironment( a:opts[ 'env' ], + \ old_env ) + endtry + + let bufnr = bufnr() + let s:buffer_to_id[ bufnr ] = id + return bufnr +endfunction + +function! s:JobIsRunning( job ) abort + return jobwait( [ a:job ], 0 )[ 0 ] == -1 +endfunction + +function! vimspector#internal#neoterm#IsFinished( bufno ) abort + if !has_key( s:buffer_to_id, a:bufno ) + return v:true + endif + + return !s:JobIsRunning( s:buffer_to_id[ a:bufno ] ) +endfunction + +function! vimspector#internal#neoterm#GetPID( bufno ) abort + if !has_key( s:buffer_to_id, a:bufno ) + return -1 + endif + + return jobpid( s:buffer_to_id[ a:bufno ] ) +endfunction + +" Boilerplate {{{ +let &cpoptions=s:save_cpo +unlet s:save_cpo +" }}} diff --git a/autoload/vimspector/internal/popup.vim b/autoload/vimspector/internal/popup.vim new file mode 100644 index 0000000..083fdf5 --- /dev/null +++ b/autoload/vimspector/internal/popup.vim @@ -0,0 +1,146 @@ +" vimspector - A multi-language debugging system for Vim +" Copyright 2018 Ben Jackson +" +" Licensed under the Apache License, Version 2.0 (the "License"); +" you may not use this file except in compliance with the License. +" You may obtain a copy of the License at +" +" http://www.apache.org/licenses/LICENSE-2.0 +" +" Unless required by applicable law or agreed to in writing, software +" distributed under the License is distributed on an "AS IS" BASIS, +" WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +" See the License for the specific language governing permissions and +" limitations under the License. +scriptencoding utf-8 + + +" Boilerplate {{{ +let s:save_cpo = &cpoptions +set cpoptions&vim +" }}} + +function! vimspector#internal#popup#DisplaySplash( message ) abort + return popup_dialog( a:message, {} ) +endfunction + +function! vimspector#internal#popup#UpdateSplash( id, message ) abort + call popup_settext( a:id, a:message ) + return a:id +endfunction + +function! vimspector#internal#popup#HideSplash( id ) abort + call popup_hide( a:id ) +endfunction + +let s:current_selection = 0 +let s:selections = [] +let s:text = [] + +function! s:UpdatePopup( id ) abort + let buf = copy( s:text ) + call extend( buf, s:DrawButtons() ) + call popup_settext( a:id, buf ) +endfunction + +function! s:ConfirmKeyFilter( keys, id, key ) abort + if a:key ==# "\" + call popup_close( a:id, s:current_selection + 1 ) + return 1 + elseif index( [ "\", "\" ], a:key ) >= 0 + let s:current_selection = ( s:current_selection + 1 ) % len( s:selections ) + call s:UpdatePopup( a:id ) + return 1 + elseif index( [ "\", "\" ], a:key ) >= 0 + let s:current_selection = s:current_selection == 0 + \ ? len( s:selections ) - 1: s:current_selection - 1 + call s:UpdatePopup( a:id ) + return 1 + elseif a:key ==# "\" || a:key ==# "\" + call popup_close( a:id, -1 ) + return 1 + endif + + let index = 1 + for key in a:keys + if a:key ==? key + call popup_close( a:id, index ) + return 1 + endif + let index += 1 + endfor +endfunction + +function! s:ConfirmCallback( confirm_id, id, result ) abort + py3 __import__( 'vimspector', fromlist = [ 'utils' ] ).utils.ConfirmCallback( + \ int( vim.eval( 'a:confirm_id' ) ), + \ int( vim.eval( 'a:result' ) ) ) +endfunction + +function! s:SelectionPosition( idx ) abort + return a:idx == 0 ? 0 : len( join( s:selections[ : a:idx - 1 ], ' ' ) ) + 1 +endfunction + +function! s:DrawButtons() abort + return [ { + \ 'text': join( s:selections, ' ' ), + \ 'props': [ + \ { + \ 'col': s:SelectionPosition( s:current_selection ) + 1, + \ 'length': len( s:selections[ s:current_selection ] ), + \ 'type': 'VimspectorSelectedItem' + \ }, + \ ] + \ } ] +endfunction + +function! vimspector#internal#popup#Confirm( + \ confirm_id, + \ text, + \ options, + \ default_value, + \ keys ) abort + + silent! call prop_type_add( 'VimspectorSelectedItem', { + \ 'highlight': 'PMenuSel' + \ } ) + + let lines = split( a:text, "\n", v:true ) + let buf = [] + for line in lines + call add( buf, { 'text': line, 'props': [] } ) + endfor + + call add( buf, { 'text': '', 'props': [] } ) + + let s:selections = a:options + let s:current_selection = ( a:default_value - 1 ) + + let s:text = copy( buf ) + call extend( buf, s:DrawButtons() ) + + let config = { + \ 'callback': function( 's:ConfirmCallback', [ a:confirm_id ] ), + \ 'filter': function( 's:ConfirmKeyFilter', [ a:keys ] ), + \ 'mapping': v:false, + \ } + let config = vimspector#internal#popup#SetBorderChars( config ) + + return popup_dialog( buf, config ) +endfunction + +function! vimspector#internal#popup#SetBorderChars( config ) abort + " When ambiwidth is single, use prettier characters for the border. This + " would look silly when ambiwidth is double. + if &ambiwidth ==# 'single' && &encoding ==? 'utf-8' + let a:config[ 'borderchars' ] = [ '─', '│', '─', '│', '╭', '╮', '┛', '╰' ] + endif + + return a:config +endfunction + + +" Boilerplate {{{ +let &cpoptions=s:save_cpo +unlet s:save_cpo +" }}} diff --git a/autoload/vimspector/internal/state.vim b/autoload/vimspector/internal/state.vim index 6478506..f1e690a 100644 --- a/autoload/vimspector/internal/state.vim +++ b/autoload/vimspector/internal/state.vim @@ -19,11 +19,32 @@ let s:save_cpo = &cpoptions set cpoptions&vim " }}} +let s:prefix = '' +if has( 'nvim' ) + let s:prefix='neo' +endif + function! vimspector#internal#state#Reset() abort - py3 << EOF -from vimspector import debug_session -_vimspector_session = debug_session.DebugSession() -EOF + try + py3 import vim + py3 _vimspector_session = __import__( + \ "vimspector", + \ fromlist=[ "debug_session" ] ).debug_session.DebugSession( + \ vim.eval( 's:prefix' ) ) + catch /.*/ + echohl WarningMsg + echom 'Exception while loading vimspector:' v:exception + echom 'From:' v:throwpoint + echom 'Vimspector unavailable: Requires Vim compiled with Python 3.6' + echohl None + return v:false + endtry + + return v:true +endfunction + +function! vimspector#internal#state#GetAPIPrefix() abort + return s:prefix endfunction " Boilerplate {{{ diff --git a/autoload/vimspector/internal/term.vim b/autoload/vimspector/internal/term.vim new file mode 100644 index 0000000..aa394ba --- /dev/null +++ b/autoload/vimspector/internal/term.vim @@ -0,0 +1,37 @@ +" vimspector - A multi-language debugging system for Vim +" Copyright 2018 Ben Jackson +" +" Licensed under the Apache License, Version 2.0 (the "License"); +" you may not use this file except in compliance with the License. +" You may obtain a copy of the License at +" +" http://www.apache.org/licenses/LICENSE-2.0 +" +" Unless required by applicable law or agreed to in writing, software +" distributed under the License is distributed on an "AS IS" BASIS, +" WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +" See the License for the specific language governing permissions and +" limitations under the License. + + +" Boilerplate {{{ +let s:save_cpo = &cpoptions +set cpoptions&vim +" }}} + +function! vimspector#internal#term#Start( cmd, opts ) abort + rightbelow return term_start( a:cmd, a:opts ) +endfunction + +function! vimspector#internal#term#IsFinished( bufno ) abort + return index( split( term_getstatus( a:bufno ), ',' ), 'finished' ) >= 0 +endfunction + +function! vimspector#internal#term#GetPID( bufno ) abort + return job_info( term_getjob( a:bufno ) ).process +endfunction + +" Boilerplate {{{ +let &cpoptions=s:save_cpo +unlet s:save_cpo +" }}} diff --git a/azure-pipelines.yml b/azure-pipelines.yml deleted file mode 100644 index 053df1f..0000000 --- a/azure-pipelines.yml +++ /dev/null @@ -1,144 +0,0 @@ -# Starter pipeline -# Start with a minimal pipeline that you can customize to build and deploy your code. -# Add steps that build, run tests, deploy, and more: -# https://aka.ms/yaml - -stages: -- stage: Build - jobs: - - job: 'PythonLint' - displayName: "Python Lint" - pool: - vmImage: 'ubuntu-16.04' - container: 'puremourning/vimspector:test' - steps: - - bash: pip3 install -r dev_requirements.txt - displayName: "Install requirements" - - - bash: $HOME/.local/bin/flake8 python3/ - displayName: "Run flake8" - - - job: 'Vimscript' - displayName: "Vimscript Lint" - pool: - vmImage: 'ubuntu-16.04' - container: 'puremourning/vimspector:test' - steps: - - bash: pip3 install -r dev_requirements.txt - displayName: "Install requirements" - - - bash: $HOME/.local/bin/vint autoload/ plugin/ - displayName: "Run vint" - - - job: 'linux' - pool: - vmImage: 'ubuntu-16.04' - container: - image: 'puremourning/vimspector:test' - options: --cap-add=SYS_PTRACE --security-opt seccomp=unconfined - steps: - - bash: | - eval $(/home/linuxbrew/.linuxbrew/bin/brew shellenv) - go get -u github.com/go-delve/delve/cmd/dlv - displayName: 'Install Delve for Go' - - - task: CacheBeta@0 - inputs: - key: v1 | gadgets | $(Agent.OS) | install_gadget.py - path: gadgets/linux/download - displayName: Cache gadgets - - - bash: python3 install_gadget.py --all - displayName: 'Install gadgets - python3' - - - bash: vim --version - displayName: 'Print vim version information' - - - bash: | - eval $(/home/linuxbrew/.linuxbrew/bin/brew shellenv) - export GOPATH=$HOME/go - ./run_tests - displayName: 'Run the tests' - env: - VIMSPECTOR_MIMODE: gdb - - - bash: ./make_package linux $(Build.SourceVersion) - displayName: 'Package' - - - task: PublishPipelineArtifact@0 - inputs: - artifactName: 'package-linux' - targetPath: 'package/linux-$(Build.SourceVersion).tar.gz' - - - job: 'macos' - pool: - vmImage: 'macOS-10.13' - steps: - - bash: | - brew unlink node@6 - brew install macvim node@10 - brew link --force --overwrite node@10 - displayName: 'Install vim and node' - - - bash: go get -u github.com/go-delve/delve/cmd/dlv - displayName: 'Install Delve for Go' - - - task: CacheBeta@0 - inputs: - key: v1 | gadgets | $(Agent.OS) | install_gadget.py - path: gadgets/macos/download - displayName: Cache gadgets - - - bash: python3 install_gadget.py --all - displayName: 'Install gadgets - python3' - - - bash: vim --version - displayName: 'Print vim version information' - - - bash: ./run_tests - displayName: 'Run the tests' - env: - VIMSPECTOR_MIMODE: lldb - - - bash: ./make_package macos $(Build.SourceVersion) - displayName: 'Package' - - - task: PublishPipelineArtifact@0 - inputs: - artifactName: 'package-macos' - targetPath: 'package/macos-$(Build.SourceVersion).tar.gz' - -- stage: "Publish" - dependsOn: - - "Build" - condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/master')) - jobs: - - job: 'Publish' - pool: - vmImage: 'ubuntu-16.04' - steps: - - task: DownloadPipelineArtifact@0 - inputs: - artifactName: 'package-linux' - targetPath: $(Build.ArtifactStagingDirectory) - - task: DownloadPipelineArtifact@0 - inputs: - artifactName: 'package-macos' - targetPath: $(Build.ArtifactStagingDirectory) - - task: GitHubRelease@0 - inputs: - gitHubConnection: puremourning - repositoryName: '$(Build.Repository.Name)' - action: 'create' # Options: create, edit, delete - target: '$(Build.SourceVersion)' # Required when action == Create || Action == Edit - tagSource: 'manual' # Required when action == Create# Options: auto, manual - tag: "$(Build.BuildId)" - #title: # Optional - #releaseNotesSource: 'file' # Optional. Options: file, input - #releaseNotesFile: # Optional - #releaseNotes: # Optional - #assets: '$(Build.ArtifactStagingDirectory)/*' # Optional - #assetUploadMode: 'delete' # Optional. Options: delete, replace - #isDraft: false # Optional - isPreRelease: true # Optional - #addChangeLog: true # Optional diff --git a/compiler/vimspector_test.vim b/compiler/vimspector_test.vim index 3396538..d0c6def 100644 --- a/compiler/vimspector_test.vim +++ b/compiler/vimspector_test.vim @@ -13,12 +13,14 @@ " See the License for the specific language governing permissions and " limitations under the License. +scriptencoding utf-8 + " Compiler plugin to help running vimspector tests -if exists("current_compiler") +if exists('current_compiler') finish endif -let current_compiler = "vimspector_test" +let current_compiler = 'vimspector_test' setlocal errorformat= \Found\ errors\ in\ %f:%.%#: @@ -35,52 +37,71 @@ if ! exists( ':' . s:make_cmd ) endif function! VimGetCurrentFunction() - echom s:GetCurrentFunction() + echom s:GetCurrentFunction()[ 0 ] endfunction function! s:GetCurrentFunction() " Store the cursor position; we'll need to reset it - let [ l:buf, l:row, l:col, l:offset ] = getpos( '.' ) + let [ buf, row, col, offset ] = getpos( '.' ) - let l:test_function = '' + let [ test_function, test_function_line ] = [ v:null, -1 ] - let l:pattern = '\V\C\s\*function!\?\s\+\(\<\w\+\>\)\.\*\$' + let pattern = '\V\C\s\*func\%\(tion\)\?!\?\s\+\(\<\w\+\>\)\.\*\$' - let l:lnum = prevnonblank( '.' ) + let lnum = prevnonblank( '.' ) " Find the top-level method and class - while l:lnum > 0 - call cursor( l:lnum, 1 ) - let l:lnum = search( l:pattern, 'bcnWz' ) + while lnum > 0 + call cursor( lnum, 1 ) + let lnum = search( pattern, 'bcnWz' ) - if l:lnum <= 0 - call cursor( l:row, l:col ) - return l:test_function + if lnum <= 0 + call cursor( row, col ) + return [ test_function, test_function_line ] endif - let l:this_decl = substitute( getline( l:lnum ), l:pattern, '\1', '' ) - let l:this_decl_is_test = match( l:this_decl, '\V\C\^Test_' ) >= 0 + let this_decl = substitute( getline( lnum ), pattern, '\1', '' ) + let this_decl_is_test = match( this_decl, '\V\C\^Test_' ) >= 0 - if l:this_decl_is_test - let l:test_function = l:this_decl + if this_decl_is_test + let [ test_function, test_function_line ] = [ this_decl, lnum ] - if indent( l:lnum ) == 0 - call cursor( l:row, l:col ) - return l:test_function + if indent( lnum ) == 0 + call cursor( row, col ) + return [ test_function, test_function_line ] endif endif - let l:lnum = prevnonblank( l:lnum - 1 ) + let lnum = prevnonblank( lnum - 1 ) endwhile + return [ v:null, -1 ] endfunction +function! s:RunTestUnderCursorInVimspector() + update + let l:test_func_name = s:GetCurrentFunction()[ 0 ] + + if l:test_func_name ==# '' + echo 'No test method found' + return + endif + + echo "Running test '" . l:test_func_name . "'" + + call vimspector#LaunchWithSettings( { + \ 'configuration': 'Run test', + \ 'TestFunction': l:test_func_name + \ } ) +endfunction + + function! s:RunTestUnderCursor() update - let l:test_func_name = s:GetCurrentFunction() + let l:test_func_name = s:GetCurrentFunction()[ 0 ] if l:test_func_name ==# '' - echo "No test method found" + echo 'No test method found' return endif @@ -90,7 +111,9 @@ function! s:RunTestUnderCursor() let l:cwd = getcwd() execute 'lcd ' . s:root_dir try - execute s:make_cmd . ' ' . l:test_arg + execute s:make_cmd . ' --report messages ' + \ . get( g:, 'vimspector_test_args', '' ) . ' ' + \ . l:test_arg finally execute 'lcd ' . l:cwd endtry @@ -101,7 +124,9 @@ function! s:RunTest() let l:cwd = getcwd() execute 'lcd ' . s:root_dir try - execute s:make_cmd . ' %:p:t' + execute s:make_cmd . ' --report messages ' + \ . get( g:, 'vimspector_test_args', '' ) + \ . ' %:p:t' finally execute 'lcd ' . l:cwd endtry @@ -112,7 +137,8 @@ function! s:RunAllTests() let l:cwd = getcwd() execute 'lcd ' . s:root_dir try - execute s:make_cmd + execute s:make_cmd . ' --report messages ' + \ . get( g:, 'vimspector_test_args', '' ) finally execute 'lcd ' . l:cwd endtry @@ -125,10 +151,36 @@ if ! has( 'gui_running' ) nnoremap  :call RunAllTests() " † is right-option+t nnoremap † :call RunTestUnderCursor() + nnoremap † :call RunTestUnderCursorInVimspector() " å is the right-option+q nnoremap å :cfirst " å is the right-option+a - nnoremap œ :cnext + nnoremap œ :FuncLine " Ω is the right-option+z nnoremap Ω :cprevious endif + +function! s:GoToCurrentFunctionLine( ... ) + if a:0 < 1 + call inputsave() + let lnum = str2nr( input( 'Enter line num: ' ) ) + call inputrestore() + else + let lnum = a:1 + endif + + let [ f, l ] = s:GetCurrentFunction() + if f is v:null + return + endif + + let lnum += l + + echo 'Function' f 'at line' l '(jump to line ' lnum . ')' + + call cursor( [ lnum, indent( lnum ) ] ) +endfunction + +command! -buffer -nargs=? -bar + \ FuncLine + \ :call s:GoToCurrentFunctionLine( ) diff --git a/dev_requirements.txt b/dev_requirements.txt index f6e9448..c7a9ec1 100644 --- a/dev_requirements.txt +++ b/dev_requirements.txt @@ -1,2 +1,6 @@ -flake8==3.7.7 -vim-vint==0.3.21 +flake8==3.8.3 +flake8-comprehensions==3.2.3 +flake8-ycm>= 0.1.0 + +# Use fork of vint which is up to date +git+https://github.com/puremourning/vint diff --git a/doc/vimspector-ref.txt b/doc/vimspector-ref.txt new file mode 100644 index 0000000..367d409 --- /dev/null +++ b/doc/vimspector-ref.txt @@ -0,0 +1,1085 @@ +*vimspector-ref* + +=============================================================================== +Contents ~ + + 1. Introduction |vimspector-ref-introduction| + 2. title: Configuration |vimspector-ref-title-configuration| + 3. Concepts |vimspector-ref-concepts| + 1. Debug adapter configuration |vimspector-ref-debug-adapter-configuration| + 2. Debug profile configuration |vimspector-ref-debug-profile-configuration| + 3. Replacements and variables |vimspector-ref-replacements-variables| + 1. The splat operator |vimspector-ref-splat-operator| + 2. Default values |vimspector-ref-default-values| + 3. Coercing Types |vimspector-ref-coercing-types| + 4. Configuration Format |vimspector-ref-configuration-format| + 5. Files and locations |vimspector-ref-files-locations| + 6. Adapter configurations |vimspector-ref-adapter-configurations| + 7. Debug configurations |vimspector-ref-debug-configurations| + 1. Configuration selection |vimspector-ref-configuration-selection| + 1. Specifying a default configuration |vimspector-ref-specifying-default-configuration| + 2. Preventing automatic selection |vimspector-ref-preventing-automatic-selection| + 2. Exception Breakpoints |vimspector-ref-exception-breakpoints| + 8. Predefined Variables |vimspector-ref-predefined-variables| + 9. Remote Debugging Support |vimspector-ref-remote-debugging-support| + 1. Python (debugpy) Example |vimspector-ref-python-example| + 2. C-family (gdbserver) Example |vimspector-ref-c-family-example| + 3. Docker Example |vimspector-ref-docker-example| + 10. Appendix: Configuration file format |vimspector-ref-appendix-configuration-file-format| + 11. Appendix: Editor configuration |vimspector-ref-appendix-editor-configuration| + 12. References |vimspector-ref-references| + +=============================================================================== + *vimspector-ref-introduction* +Introduction ~ + +=============================================================================== + *vimspector-ref-title-configuration* +title: Configuration ~ + +This document defines the supported format for project and adapter +configuration for Vimspector. + +- Concepts + + - Debug adapter configuration + - Debug profile configuration + - Replacements and variables + - The splat operator + - Default values + - Coercing Types + +- Configuration Format +- Files and locations +- Adapter configurations +- Debug configurations + + - Configuration selection + + - Specifying a default configuration + - Preventing automatic selection + + - Exception Breakpoints + +- Predefined Variables +- Remote Debugging Support + + - Python (debugpy) Example + - C-family (gdbserver) Example + - Docker Example + +- Appendix: Configuration file format +- Appendix: Editor configuration + +=============================================================================== + *vimspector-ref-concepts* +Concepts ~ + +As Vimspector supports debugging arbitrary projects, you need to tell it a few +details about what you want to debug, and how to go about doing that. + +In order to debug things, Vimspector requires a Debug Adapter which bridges +between Vimspector and the actual debugger tool. Vimspector can be used with +any debug adapter that implements the Debug Adapter Protocol [1]. + +For each debugging session, you provide a _debug configuration_ which includes +things like: + +- The debug adapter to use (and possibly how to launch and configure it). +- How to connect to the remote host, if remote debugging. +- How to launch or attach to your process. + +Along with optional additional configuration for things like: + +- Exception breakpoints + +------------------------------------------------------------------------------- + *vimspector-ref-debug-adapter-configuration* +Debug adapter configuration ~ + +The adapter to use for a particular debug session can be specified inline +within the _debug configuration_, but more usually the debug adapter is defined +separately and just referenced from the _debug configuration_. + +The adapter configuration includes things like: + +- How to launch or connect to the debug adapter +- How to configure it for PID attachment +- How to set up remote debugging, such as how to launch the process remotely + (for example, under 'gdbserver', 'ptvsd', etc.) + +------------------------------------------------------------------------------- + *vimspector-ref-debug-profile-configuration* +Debug profile configuration ~ + +Projects can have many different debug profiles. For example you might have all +of the following, for a given source tree: + +- Remotely launch c++ the process, and break on 'main' +- Locally Python test and break exception +- Remotely attach to a c++ process +- Locally launch a bash script +- Attach to a JVM listening on a port + +Each of these represents a different use case and a different _debug +configuration_. As mentioned above, a _debug configuration_ is essentially: + +- The adapter to use + +- The type of session (launch or attach), and whether or not to do it + remotely + +- The configuration to pass to the adapter in order to launch or attach to + the process. + +The bulk of the configuration is the last of these, which comprises +adapter-specific options, as the Debug Adapter Protocol does not specify any +standard for launch or attach configuration. + +------------------------------------------------------------------------------- + *vimspector-ref-replacements-variables* +Replacements and variables ~ + +Vimspector _debug configuration_ is intended to be as general as possible, and +to be committed to source control so that debugging your applications becomes a +simple, quick and pain-free habit (e.g. answering questions like "what happens +if..." with "just hit F5 and step through!"). + +Therefore it's important to abstract certain details, like runtime and +build-time paths, and to parameterise the _debug configuration_. Vimspector +provides a simple mechanism to do this with '${replacement}' style +replacements. + +The values available within the '${...}' are defined below, but in summary the +following are supported: + +- Environment variables, such as '${PATH}' +- Predefined variables, such as '${workspaceRoot}', '${file}' etc. +- Configuration-defined variables, either provided by the adapter + configuration or debug configuration, or from running a simple shell + command. +- Anything else you like - the user will be asked to provide a value. + +If the latter 2 are confusing, for now, suffice to say that they are how +Vimspector allows parameterisation of debug sessions. The [Vimspector +website][website-getting-started] has a good example of where this sort of +thing is useful: accepting the name of a test to run. + +But for now, consider the following example snippet: +> + { + "configurations": { + "example-debug-configuration": { + // This is a single-line comment explaining the purpose + "adapter": "example-adapter-name", + "variables": { + "SecretToken": { // Variables should start with upper-case letters + "shell" : [ "cat", "${HOME}/.secret_token" ] + } + }, + "configuration": { + "request": "launch" /* or it could be "attach" */, + "program": [ + "${fileBasenameNoExtension}", + "-c", "configuration_file.cfg", + "-u", "${USER}", + "--test-identifier", "${TestIdentifier}", + "--secret-token", "${SecretToken}" + ] + }, + "breakpoints": { + "exception": { + "caught": "", + "uncaught": "Y" + } + } + } + } + } +< +In this (fictitious) example the 'program' launch configuration item contains +the following variable substitutions: + +- '${fileBasenameNoExtension}' - this is a Predefined Variable, set by + Vimspector to the base name of the file that's opened in Vim, with its + extension removed ('/path/to/xyz.cc' -> 'xyz'). + +- '${USER}' - this refers to the Environment Variable 'USER'. + +- '${TestIdentifier}' - this variable is not defined, so the user is asked to + provide a value interactively when starting debugging. Vimspector remembers + what they said and provides it as the default should they debug again. + +- '${SecretToken}' - this variable is provided by the configuration's + 'variables' block. Its value is taken from the 'strip''d result of running + the shell command. Note these variables can be supplied by both the debug + and adapter configurations and can be either static strings or shell + commands. + +------------------------------------------------------------------------------- + *vimspector-ref-splat-operator* +The splat operator ~ + +Often we want to create a single '.vimspector.json' entry which encompasses +many use cases, as it is tedious to write every use case/start up option in +JSON. This is why we have the replacement variables after all. + +Frequently debug adapters request command arguments as a JSON array, for +example: +> + "args": [ "one", "two three", "four" ], +< +To help with this sort of case, Vimspector supports a 'splat' operator for +replacement variables operating within lists. The syntax is: '"*${var}', which +means roughly "splice the contents of '${var}' into the list at this position". +'${var}' is parsed like a shell command (using python's 'shlex' parser) and +each word is added as a list item. + +For example: +> + "args": [ "*${CommandLineArgs}" ] +< +This would: + +- Ask the user to provide the variable 'CommandLineArgs'. Let's say they + entered 'one "two three" four' +- Split 'CommandLineArgs' like shell arguments: 'one', 'two three' and 'four' +- Set 'args' in the settings dict to: '[ "one", "two three", "four" ]' + +You can also combine with static values: +> + "args": [ "First", "*${CommandLineArgs}", "Last" ] +< +This would yield the intuitive result: '[ "First", "one", "two three", "four", +"Last" ]' + +------------------------------------------------------------------------------- + *vimspector-ref-default-values* +Default values ~ + +You can specify replacements with default values. In this case if the user has +not specified a value, they are prompted but with the default value +pre-populated, allowing them to just press return to accept the default. + +The syntax is '${variableName:default value}'. The default value can contain +any character, but to include a '}' you must escape it with a backslash. To +include a backslash in the JSON you must write '\\', as in: +> + { "key": "${value:default {\\} stuff}" } +< +The default value can also be a replacement variable. However, this _must_ be a +variable that's already defined, such as one of the predefined variables, or +one specified in a 'variables' block. In order to reference them, you _must_ +use '${var}' syntax and you _must_ escape the closing '}'. For example, the is +a common and useful case: +> + { + "configuration": { + "program": "${script:${file\\}}" + } + } +< +This will prompt the user to specify 'script', but it will default to the path +to the current file. + +------------------------------------------------------------------------------- + *vimspector-ref-coercing-types* +Coercing Types ~ + +Sometimes, you want to provide an option for a boolean parameter, or want to +allow the user to specify more than just strings. Vimspector allows you to do +this, ensuring that the resulting JSON is valid. This is done by interpreting a +value as a JSON string and substituting the resulting JSON value in its place. + +This is easier to explain with an example. Let's say we want to offer the +ability to break on entry, as an option for the user. The launch configuration +requires 'stopOnEntry' to be a bool. This doesn't work: +> + "stopOnEntry": "${StopOnEntry}" +< +The reason is that if the user types 'true', the resulting object is: +> + "stopOnEntry": "true" +< +The problem being that is a string, not a boolean. So Vimspector allows you to +re-interpret the string as a JSON value and use that instead. To do this, add +'#json' to the key's name. You can even add a default, like this: +> + "stopOnEntry#json": "${stopOnEntry:true}" +< +If the user accepts the default, the resulting string '"true"' is coerced to a +JSON value 'true', and the suffix is stripped fom the key, resulting in the +following: +> + "stopOnEntry#json": true +< +Which is what we need. + +If you happen to have a key that already ends in '#json' (unlikely!), then you +can force Vimspector to treat the value as a string by appending '#s', as in: +> + "unlikelyKeyName#json#s": "this is a string, not JSON data" +< +**_Advanced usage:_** + +The most common usage for this is for number and bool types, but it works for +objects too. If you want to be able to specify a whole object (e.g. a whole +'env' dict), then you can do that too: +> + "env#json": "${Environment:{\\}}" +< +The default value here is '{}' (note the '}' must be escaped!). The user can +then enter something like '{ "MYVAR": "MyValue", "OTHER": "Other" }' and the +resulting object would be: +> + "env": { + "MYVAR": "MyValue", + "OTHER": "Other" + } +< +It also works for lists, though the splat operator is usually more convenient +for that. + +=============================================================================== + *vimspector-ref-configuration-format* +Configuration Format ~ + +All Vimspector configuration is defined in a JSON object. The complete +specification of this object is available in the JSON Schema [2], but the basic +format for the configuration object is: +> + { + "adapters": { }, + "configurations": { } + } +< +The 'adapters' key is actually optional, as '' can be +embedded within '', though this is not recommended usage. + +=============================================================================== + *vimspector-ref-files-locations* +Files and locations ~ + +The above configuration object is constructed from a number of configuration +files, by merging objects in a specified order. + +In a minimal sense, the only file required is a '.vimspector.json' file in the +root of your project which defines the full configuration object [2], but it is +usually useful to split the 'adapters' configuration into a separate file (or +indeed one file per debug adapter). + +The following sections describe the files that are read and use the following +abbreviations: + +- '' means the path to the Vimspector installation (such as + '$HOME/.vim/pack/vimspector/start/vimspector') + +- '' is either 'macos' or 'linux' depending on the host operating system. + +- '' is the Vim filetype. Where multiple filetypes are in effect, + typically all filetypes are checked. + +=============================================================================== + *vimspector-ref-adapter-configurations* +Adapter configurations ~ + +Vimspector reads a series of files to build the 'adapters' object. The +'adapters' objects are merged in such a way that a definition for an adapter +named 'example-adapter' in a later file _completely replaces_ a previous +definition. + +- '/gadgets//.gadgets.json' - the file written by + 'install_gadget.py' and not usually edited by users. + +- '/gadgets//.gadgets.d/*.json' (sorted alphabetically). + These files are user-supplied and override the above. + +- The first such '.gadgets.json' file found in all parent directories of the + file open in Vim. + +- The '.vimspector.json' and any filetype-specific configurations (see below) + +In all cases, the required format is: +> + { + "$schema": "https://puremourning.github.io/vimspector/schema/gadgets.schema.json#", + "adapters": { + "": { + + } + } + } +< +Each adapters block can define any number of adapters. As mentioned, if the +same adapter name exists in multiple files, the last one read takes precedence +and _completely replaces_ the previous configuration. In particular that means +you can't just override one option, you have to override the whole block. + +Adapter configurations are re-read at the start of each debug session. + +The specification for the gadget object is defined in the [gadget schema][]. + +=============================================================================== + *vimspector-ref-debug-configurations* +Debug configurations ~ + +There are two locations for debug configurations for a project: + +- '/configurations///*.json' +- '.vimspector.json' in the project source + +Typically, the debug configurations are read from '.vimspector.json'. The file +is found (like '.gadgets.json' above) by recursively searching up the directory +hierarchy from the directory of the file open in Vim. The first file found is +read and no further searching is done. + +Only a single '.vimspector.json' is read. If one is found, the location of this +file is used for '${workspaceRoot}' and other workspace-relative paths. + +In addition, users can create filetype-specific configurations in the +Vimspector installation directory. This can be useful where the parameters for +the debug session for a particular filetype are always known in advance, or can +always be entered by the user. This allows for debugging to "just work" without +any modification to the project source (no need to add a '.vimspector.json'). +In this case, the '${workspaceRoot}' and workspace-relative paths are +interpreted relative to the file open in Vim. This isn't ideal, but there is no +other obvious way to default this variable. + +As with gadgets, any debug configurations appearing within '.vimspector.json' +override any that appear in the common configuration dir. + +Debug configurations are re-read at the start of each debug session, so +modifications are picked up without any restarts of Vim. + +The specification for the gadget object is defined in the schema [2], but a +typical example looks like this: +> + { + "$schema": "https://puremourning.github.io/vimspector/schema/vimspector.schema.json#", + "configurations": { + "": { + "adapter": "", + "configuration": { + "request": "", + + } + } + } + } +< +------------------------------------------------------------------------------- + *vimspector-ref-configuration-selection* +Configuration selection ~ + +When starting debugging, you can specify which debug configuration to launch +with "call vimspector#LaunchWithSettings( #{ configuration: 'name here' } )". + +Otherwise, if there's only one configuration found, Vimspector will use that +configuration, unless it contains a key '"autoselect": false'. + +If multiple debug configurations are found, and no explicit configuration was +selected on Launch, the user is prompted to select a configuration, unless a +single debug configuration is found with a key '"default": true'. + +------------------------------------------------------------------------------- + *vimspector-ref-specifying-default-configuration* +Specifying a default configuration ~ + +As noted, you can specify a default configuration with '"default": true': +> + { + "configurations": { + "use this one": { + "default": true, + "adapter": " ... ", + "configuation": { + // ... + } + }, + "don't use this one": { + // ... + } + } + } +< +If multiple configurations are found with 'default' set to 'true', then the +user is prompted anyway. + +------------------------------------------------------------------------------- + *vimspector-ref-preventing-automatic-selection* +Preventing automatic selection ~ + +If you don't want a configuration to be selected automatically, then set +'"autoselect": false'. This particularly useful for configurations in the +central (as opposed to project-local) directory. For example: +> + "configurations": { + "Don't use this by default!": { + "autoselect": false, + "adapter": " ... ", + "configuation": { + // ... + } + } + } +< +Setting 'autoselect' to 'false' overrides setting 'default' to 'true'. + +------------------------------------------------------------------------------- + *vimspector-ref-exception-breakpoints* +Exception Breakpoints ~ + +Debug adapters have arbitrary configuration for exception breakpoints. Normally +this is presented as a series of question to the user on starting the debug +session. The question includes the name of the exception breakpoint option, the +default and the list of valid responses (usually 'Y' or 'N'). + +You can pre-configure the answers to these questions in the 'breakpoints' +section of the debug configuration. For each question, take the name provided +and configure the response 'exception' mapping in the 'breakpoints' mapping. If +the configured response is empty string, the debug adapter default will be +used. + +Referring to the above example, the following tells the debug adapter to use +the default value for 'caught' exceptions and to break on 'uncaught' exception: +> + { + "configurations": { + "example-debug-configuration": { + "adapter": "example-adapter-name", + "breakpoints": { + "exception": { + "caught": "", + "uncaught": "Y" + } + }, + ... +< +The keys in the 'exception' mapping are what Vimspector includes in the prompt. +For example, when prompted with the following: +> + cpp_throw: Break on C++: on throw (Y/N/default: Y)? +< +The exception breakpoint "type" is 'cpp_throw' and the default is 'Y'. + +Similarly: +> + cpp_catch: Break on C++: on catch (Y/N/default: N)? +< +The exception breakpoint "type" is 'cpp_catch' and the default is 'N'. + +Use the following to set the values in configuration and not get asked: +> + "configurations": { + "example-debug-configuration": { + "adapter": "example-adapter-name", + "breakpoints": { + "exception": { + "cpp_throw": "Y", + "cpp_catch": "Y" + } + }, +< +To just accept the defaults for these exception breakpoint types, don't specify +a value, as in : +> + "configurations": { + "example-debug-configuration": { + "adapter": "example-adapter-name", + "breakpoints": { + "exception": { + "cpp_throw": "", + "cpp_catch": "" + } + }, +< +=============================================================================== + *vimspector-ref-predefined-variables* +Predefined Variables ~ + +The following variables are provided: + +- '${dollar}' - has the value '$', can be used to enter a literal dollar +- '$$' - a literal dollar +- '${workspaceRoot}' - the path of the folder where '.vimspector.json' was + found +- '${workspaceFolder}' - the path of the folder where '.vimspector.json' was + found +- '${gadgetDir}' - path to the OS-specific gadget dir ('/gadgets/') +- '${file}' - the current opened file +- '${relativeFile}' - the current opened file relative to 'workspaceRoot' +- '${fileBasename}' - the current opened file's 'basename' +- '${fileBasenameNoExtension}' - the current opened file's 'basename' with no + file extension +- '${fileDirname}' - the current opened file's 'dirname' +- '${fileExtname}' - the current opened file's extension +- '${cwd}' - the current working directory of the active window on launch +- '${unusedLocalPort}' - an unused local TCP port + +=============================================================================== + *vimspector-ref-remote-debugging-support* +Remote Debugging Support ~ + +Vimspector has in-built support for executing remote debuggers (such as +'gdbserver', 'debugpy', 'llvm-server' etc.). This is useful for environments +where the development is done on one host and the runtime is some other host, +account, container, etc. + +In order for it to work, you have to set up paswordless SSH between the local +and remote machines/accounts. Then just tell Vimspector how to remotely launch +and/or attach to the app. + +This is presented as examples with commentary, as it's a fairly advanced/niche +case. If you're not already familiar with remote debugging tools (such as +gdbserver) or not familiar with ssh or such, you might need to independently +research that. + +Vimspector's tools are intended to automate your existing process for setting +this up rather than to offer batteries-included approach. Ultimately, all +Vimspector is going to do is run your commands over SSH, or docker, and +co-ordinate with the adapter. + +------------------------------------------------------------------------------- + *vimspector-ref-python-example* +Python (debugpy) Example ~ + +Here is some examples using the Vimspector built-in remote support (using SSH) +to remotely launch and attach a python application and connect to it using +debugpy. + +The usage pattern is to hit '', enter 'host' (the host where your app +runs), 'account' (the account it runs under), and 'port' (a port that will be +opened on the remote host). Vimspector also supports exec'ing into Docker run +containers with 'container' (the container name or id your app is running in). +Vimspector then orchestrates the various tools to set you up. +> + { + "adapters": { + "python-remote": { + "port": "${port}", + "host": "${host}", + "launch": { + "remote": { + "host": "${host}", // Remote host to ssh to (mandatory if not using container) + "account": "${account}", // User to connect as (optional) + + // Optional.... Manual additional arguments for ssh + // "ssh": { + // "args": [ "-o", "StrictHostKeyChecking=no" ] + // }, + + // Command to launch the debugee and attach the debugger; + // %CMD% replaced with the remote-cmdLine configured in the launch + // configuration. (mandatory) + "runCommand": [ + "python", "-m", "debugpy", + "--listen", "0.0.0.0:${port}", + "--wait-for-client", + "%CMD%" + ] + + // Optional alternative to runCommand (if you need to run multiple + // commands) + // "runCommands": [ + // [ /* first command */ ], + // [ /* second command */ ] + // ] + + } + + // optional delay to wait after running runCommand(s). This is often + // needed because of the way docker handles TCP, or if you're using some + // wrapper (e.g. to start the JVM) + // "delay": "1000m" // format as per :help sleep + }, + "attach": { + "remote": { + "host": "${host}", // Remote host to ssh to (mandatory if not using container) + "account": "${account}", // User to connect as (optional) + // Command to get the PID of the process to attach (mandatory) + "pidCommand": [ + // + // Remember taht you can use ${var} to ask for input. I use this to + // call a custom command to returm the PID for a named service, so + // here's an examle: + // + "/path/to/secret/script/GetPIDForService", "${ServiceName}" + ], + + // Command to attach the debugger; %PID% replaced with output of + // pidCommand above (mandatory) + "attachCommand": [ + "python", "-m", "debugpy", "--listen", "0.0.0.0:${port}", + "--pid", "%PID%" + ] + + // Optional alternative to attachCommand (if you need to run multiple + // commands) + // "attachCommands": [ + // [ /* first command */ ], + // [ /* second command */ ] + // ], + + // Optional.... useful with buggy gdbservers to kill -TRAP %PID% + // "initCompleteCommand": [ + // /* optional command to run after initialized */ + // ] + + // Optional.... Manual additional arguments for ssh + // "ssh": { + // "args": [ "-o", "StrictHostKeyChecking=no" ] + // }, + } + // optional delay to wait after running runCommand(s). This is often + // needed because of the way docker handles TCP, or if you're using some + // wrapper (e.g. to start the JVM) + // "delay": "1000m" // format as per :help sleep + } + } + }, + "configurations": { + "remote-launch": { + "adapter": "python-remote", + + "remote-request": "launch", + "remote-cmdLine": [ + "${RemoteRoot}/${fileBasename}", "*${args}" + ], + + "configuration": { + "request": "attach", + "pathMappings": [ + { + "localRoot": "${workspaceRoot}", + "remoteRoot": "${RemoteRoot}" + } + ] + } + }, + "remote-attach": { + "variables": { + // Just an example of how to specify a variable manually rather than + // vimspector asking for input from the user + "ServiceName": "${fileBasenameNoExtention}" + }, + + "adapter": "python-remote", + "remote-request": "attach", + + "configuration": { + "request": "attach", + "pathMappings": [ + { + "localRoot": "${workspaceRoot}", + "remoteRoot": "${RemoteRoot}" + } + ] + } + } + } + } +< +------------------------------------------------------------------------------- + *vimspector-ref-c-family-example* +C-family (gdbserver) Example ~ + +This example uses Vimspector to remotely launch or attach to a binary using +'gdbserver' and then instructs vscode-cpptools to attach to that 'gdbserver'. + +The approach is very similar to the above for python, just that we use +gdbserver and have to tell cpptools a few more options. +> + { + "adapters": { + "cpptools-remote": { + "command": [ + "${gadgetDir}/vscode-cpptools/debugAdapters/OpenDebugAD7" + ], + "name": "cppdbg", + "configuration": { + "type": "cppdbg" + }, + "launch": { + "remote": { + "host": "${host}", + "account": "${account}", + "runCommand": [ + "gdbserver", + "--once", + "--no-startup-with-shell", + "--disable-randomisation", + "0.0.0.0:${port}", + "%CMD%" + } + }, + "attach": { + "remote": { + "host": "${host}", + "account": "${account}", + "pidCommand": [ + "/path/to/secret/script/GetPIDForService", "${ServiceName}" + ], + "attachCommand": [ + "gdbserver", + "--once", + "--attach", + "0.0.0.0:${port}", + "%PID%" + ], + // + // If your application is started by a wrapper script, then you might + // need the followin. GDB can't pause an application because it only + // sends the signal to the process group leader. Or something. + // Basically, if you find that everything just hangs and the + // application never attaches, try using the following to manually + // force the trap signal. + // + "initCompleteCommand": [ + "kill", + "-TRAP", + "%PID%" + ] + } + } + } + }, + "configurations": { + "remote launch": { + "adapter": "cpptools-remote", + "remote-cmdLine": [ "/path/to/the/remote/executable", "args..." ], + "remote-request": "launch", + "configuration": { + "request": "attach", // yes, attach! + + "program": "/path/to/the/local/executable", + "MIMode": "gdb", + "miDebuggerAddress": "${host}:${port}" + } + }, + "remote attach": { + "adapter": "cpptools-remote", + "remote-request": "attach", + "configuration": { + "request": "attach", + + "program": "/path/to/the/local/executable", + "MIMode": "gdb", + "miDebuggerAddress": "${host}:${port}" + } + } + } +< +------------------------------------------------------------------------------- + *vimspector-ref-docker-example* +Docker Example ~ + +This example uses Vimspector to remotely launch or attach to a docker container +port. +> + { + "adapters": { + "python-remote": { + "port": "${port}", + "launch": { + "remote": { + "container": "${container}", // Docker container id or name to exec into to. + + // Command to launch the debugee and attach the debugger; + // %CMD% replaced with the remote-cmdLine configured in the launch + // configuration. (mandatory) + "runCommand": [ + "python", "-m", "debugpy", + "--listen", "0.0.0.0:${port}", + "--wait-for-client", + "%CMD%" + ] + + // Optional alternative to runCommand (if you need to run multiple + // commands) + // "runCommands": [ + // [ /* first command */ ], + // [ /* second command */ ] + // ] + + } + + // optional delay to wait after running runCommand(s). This is often + // needed because of the way docker handles TCP + "delay": "1000m" // format as per :help sleep + }, + "attach": { + "remote": { + "container": "${container}", // Docker container id or name to exec into. + // Command to get the PID of the process to attach (mandatory) + // This command gets appended to "docker exec ${container}" + "pidCommand": [ + // + // Remember taht you can use ${var} to ask for input. I use this to + // call a custom command to returm the PID for a named service, so + // here's an examle: + // + "sh", "-c", "pgrep", "-f", "${filename}" + ], + + // Command to attach the debugger; %PID% replaced with output of + // pidCommand above (mandatory) + "attachCommand": [ + "sh", "-c", "python", "-m", "debugpy", "--listen", "0.0.0.0:${port}", + "--pid", "%PID%" + ] + + // Optional alternative to attachCommand (if you need to run multiple + // commands) + // "attachCommands": [ + // [ /* first command */ ], + // [ /* second command */ ] + // ], + + // Optional.... useful with buggy gdbservers to kill -TRAP %PID% + // "initCompleteCommand": [ + // /* optional command to run after initialized */ + // ] + + } + + // optional delay to wait after running runCommand(s). This is often + // needed because of the way docker handles TCP, or if you're using some + // wrapper (e.g. to start the JVM) + "delay": "1000m" // format as per :help sleep + } + } + }, + "configurations": { + "remote-launch": { + "adapter": "python-remote", + + "remote-request": "launch", + "remote-cmdLine": [ + "${RemoteRoot}/${fileBasename}", "*${args}" + ], + + "configuration": { + "request": "attach", + "pathMappings": [ + { + "localRoot": "${workspaceRoot}", + "remoteRoot": "${RemoteRoot}" + } + ] + } + }, + "remote-attach": { + "variables": { + // Just an example of how to specify a variable manually rather than + // vimspector asking for input from the user + "FileName": "${fileName}" + }, + + "adapter": "python-remote", + "remote-request": "attach", + + "configuration": { + "request": "attach", + "pathMappings": [ + { + "localRoot": "${workspaceRoot}", + "remoteRoot": "${RemoteRoot}" + } + ] + } + } + } + } +< +=============================================================================== + *vimspector-ref-appendix-configuration-file-format* +Appendix: Configuration file format ~ + +The configuration files are text files which must be UTF-8 encoded. They +contain a single JSON object, along with optional comments. + +Comments are "c-style", i.e.: + +- '// single line comment ...' +- '/* inline comment */' + +There is much debate about whether JSON files should contain comments. I have +added them because they are useful in the context of configuration files. +Unforutnately this may mean your editor doesn't like them (they are strictly +invalid JSON) so it's up to you if you use them. + +Technically, Vimspector uses JSON minify [3] to strip comments before parsing +the JSON. + +=============================================================================== + *vimspector-ref-appendix-editor-configuration* +Appendix: Editor configuration ~ + +If you would like some assistance with writing the JSON files, and your editor +of choice has a way to use a language server, you can use the VSCode JSON +language server [4]. + +It is recommended to include the '$schema' declaration as in the above +examples, but if that isn't present, the following JSON language server +configuration [5] is recommened to load the schema from the Internet: +> + { + "json": { + "schemas": [ + { + "fileMatch": [ ".vimspector.json" ], + "url": "https://puremourning.github.io/vimspector/schema/vimspector.schema.json" + }, + { + "fileMatch": [ ".gadgets.json", ".gadgets.d/*.json" ], + "url": "https://puremourning.github.io/vimspector/schema/gadgets.schema.json" + } + ] + } + } +< +If your language server client of choice happens to be YouCompleteMe [6], then +the following '.ycm_extra_conf.py' is good enough to get you going, after +following the instructions in the lsp-examples [7] repo to get the server set +up: +> + VIMSPECTOR_HOME = '/path/to/vimspector' # TODO: Change this + + def Settings( **kwargs ): + if kwargs[ 'language' ] == 'json': + return { + 'ls': { + 'json': { + 'schemas': [ + { + 'fileMatch': [ '.vimspector.json' ], + 'url': f'file://{VIMSPECTOR_HOME}/docs/schema/vimspector.schema.json' + }, + { + 'fileMatch': [ '.gadgets.json', '.gadgets.d/*.json' ], + 'url': f'file://{VIMSPECTOR_HOME}/docs/schema/gadgets.schema.json' + } + ] + } + } + } + + return None # Or your existing Settings definition.... +< +This configuration can be adapted to any other LSP-based editor configuration +and is provided just as an example. + +=============================================================================== + *vimspector-ref-references* +References ~ + +[1] https://microsoft.github.io/debug-adapter-protocol/ +[2] http://puremourning.github.io/vimspector/schema/vimspector.schema.json +[3] https://github.com/getify/JSON.minify +[4] https://github.com/vscode-langservers/vscode-json-languageserver +[5] https://github.com/vscode-langservers/vscode-json-languageserver#settings +[6] https://github.com/ycm-core/YouCompleteMe +[7] https://github.com/ycm-core/lsp-examples + +vim: ft=help diff --git a/doc/vimspector.txt b/doc/vimspector.txt new file mode 100644 index 0000000..8def87f --- /dev/null +++ b/doc/vimspector.txt @@ -0,0 +1,2394 @@ +*vimspector* vimspector - A multi language graphical debugger for Vim + +=============================================================================== +Contents ~ + + 1. Introduction |vimspector-introduction| + 2. Features and Usage |vimspector-features-usage| + 1. Supported debugging features |vimspector-supported-debugging-features| + 2. Supported languages |vimspector-supported-languages| + 3. Other languages |vimspector-other-languages| + 3. Installation |vimspector-installation| + 1. Quick Start |vimspector-quick-start| + 2. Dependencies |vimspector-dependencies| + 3. Neovim differences |vimspector-neovim-differences| + 4. Windows differences |vimspector-windows-differences| + 5. Trying it out |vimspector-trying-it-out| + 6. Cloning the plugin |vimspector-cloning-plugin| + 7. Install some gadgets |vimspector-install-gadgets| + 1. VimspectorInstall and VimspectorUpdate commands |vimspectorinstall-vimspectorupdate-commands| + 2. install_gadget.py |vimspector-install_gadget.py| + 8. Manual gadget installation |vimspector-manual-gadget-installation| + 1. The gadget directory |vimspector-gadget-directory| + 9. Upgrade |vimspector-upgrade| + 4. About |vimspector-about| + 1. Background |vimspector-background| + 2. Status |vimspector-status| + 1. Experimental |vimspector-experimental| + 3. Motivation |vimspector-motivation| + 4. License |vimspector-license| + 5. Sponsorship |vimspector-sponsorship| + 5. Mappings |vimspector-mappings| + 1. Visual Studio / VSCode |vimspector-visual-studio-vscode| + 2. Human Mode |vimspector-human-mode| + 6. Usage and API |vimspector-usage-api| + 1. Launch and attach by PID: |vimspector-launch-attach-by-pid| + 1. Launch with options |vimspector-launch-with-options| + 2. Debug configuration selection |vimspector-debug-configuration-selection| + 3. Get configurations |vimspector-get-configurations| + 2. Breakpoints |vimspector-breakpoints| + 1. Summary |vimspector-summary| + 2. Line breakpoints |vimspector-line-breakpoints| + 3. Conditional breakpoints |vimspector-conditional-breakpoints| + 4. Exception breakpoints |vimspector-exception-breakpoints| + 5. Clear breakpoints |vimspector-clear-breakpoints| + 6. Run to Cursor |vimspector-run-to-cursor| + 3. Stepping |vimspector-stepping| + 4. Variables and scopes |vimspector-variables-scopes| + 5. Variable or selection hover evaluation |vimspector-variable-or-selection-hover-evaluation| + 6. Watches |vimspector-watches| + 1. Watch autocompletion |vimspector-watch-autocompletion| + 7. Stack Traces |vimspector-stack-traces| + 8. Program Output |vimspector-program-output| + 1. Console |vimspector-console| + 2. Console autocompletion |vimspector-console-autocompletion| + 3. Log View |vimspector-log-view| + 9. Closing debugger |vimspector-closing-debugger| + 10. Terminate debuggee |vimspector-terminate-debuggee| + 7. Debug profile configuration |vimspector-debug-profile-configuration| + 1. C, C++, Rust, etc. |vimspector-c-c-rust-etc.| + 1. C++ Remote debugging |vimspector-c-remote-debugging| + 2. C++ Remote launch and attach |vimspector-c-remote-launch-attach| + 2. Rust |vimspector-rust| + 3. Python |vimspector-python| + 1. Python Remote Debugging |vimspector-python-remote-debugging| + 2. Python Remote launch and attach |vimspector-python-remote-launch-attach| + 3. Legacy: vscode-python |vimspector-legacy-vscode-python| + 4. TCL |vimspector-tcl| + 5. C♯ |vimspector-c| + 6. Go |vimspector-go| + 7. PHP |vimspector-php| + 1. Debug web application |vimspector-debug-web-application| + 2. Debug cli application |vimspector-debug-cli-application| + 8. JavaScript, TypeScript, etc. |vimspector-javascript-typescript-etc.| + 9. Java |vimspector-java| + 1. Usage with YouCompleteMe |vimspector-usage-with-youcompleteme| + 2. Other LSP clients |vimspector-other-lsp-clients| + 10. Lua |vimspector-lua| + 11. Other servers |vimspector-other-servers| + 8. Customisation |vimspector-customisation| + 1. Changing the default signs |vimspector-changing-default-signs| + 2. Sign priority |vimspector-sign-priority| + 3. Changing the default window sizes |vimspector-changing-default-window-sizes| + 4. Changing the terminal size |vimspector-changing-terminal-size| + 5. Custom mappings while debugging |vimspector-custom-mappings-while-debugging| + 6. Advanced UI customisation |vimspector-advanced-ui-customisation| + 7. Customising the WinBar |vimspector-customising-winbar| + 8. Example |vimspector-example| + 9. FAQ |vimspector-faq| + 10. References |vimspector-references| + +=============================================================================== + *vimspector-introduction* +Introduction ~ + +For a tutorial and usage overview, take a look at the Vimspector website [1]. + +For detailed explanatin of the '.vimspector.json' format, see the reference +guide [2]. + +Image: Build (see reference [3]) Image: Gitter [4] + +- Features and Usage + + - Supported debugging features + - Supported languages + - Other languages + +- Installation + + - Quick Start + - Dependencies + - Neovim differences + - Windows differences + - Trying it out + - Cloning the plugin + - Install some gadgets + - VimspectorInstall and VimspectorUpdate commands + - install_gadget.py + - Manual gadget installation + - The gadget directory + - Upgrade + +- About + + - Background + - Status + - Experimental + - Motivation + - License + - Sponsorship + +- Mappings + + - Visual Studio / VSCode + - Human Mode + +- Usage and API + + - Launch and attach by PID: + - Launch with options + - Debug configuration selection + - Get configurations + - Breakpoints + - Summary + - Line breakpoints + - Conditional breakpoints + - Exception breakpoints + - Clear breakpoints + - Run to Cursor + - Stepping + - Variables and scopes + - Variable or selection hover evaluation + - Watches + - Watch autocompletion + - Stack Traces + - Program Output + - Console + - Console autocompletion + - Log View + - Closing debugger + +- Debug profile configuration + + - C, C , Rust, etc. + - C Remote debugging + - C Remote launch and attach + - Rust + - Python + - Python Remote Debugging + - Python Remote launch and attach + - Legacy: vscode-python + - TCL + - C♯ + - Go + - PHP + - Debug web application + - Debug cli application + - JavaScript, TypeScript, etc. + - Java + - Usage with YouCompleteMe + - Other LSP clients + - Lua + - Other servers + +- Customisation + + - Changing the default signs + - Sign priority + - Changing the default window sizes + - Changing the terminal size + - Custom mappings while debugging + - Advanced UI customisation + - Customising the WinBar + - Example + +- FAQ + +=============================================================================== + *vimspector-features-usage* +Features and Usage ~ + +The plugin is a capable Vim graphical debugger for multiple languages. It's +mostly tested for c++, python and TCL, but in theory supports any language that +Visual Studio Code supports (but see caveats). + +The Vimspector website [1] has an overview of the UI, along with basic +instructions for configuration and setup. + +But for now, here's a (rather old) screenshot of Vimspector debugging Vim: + + Image: vimspector-vim-screenshot (see reference [6]) + +And a couple of brief demos: + + Image: asciicast [7] + + Image: asciicast [9] + +------------------------------------------------------------------------------- + *vimspector-supported-debugging-features* +Supported debugging features ~ + +- flexible configuration syntax that can be checked in to source control +- breakpoints (function, line and exception breakpoints) +- conditional breakpoints (function, line) +- step in/out/over/up, stop, restart +- run to cursor +- launch and attach +- remote launch, remote attach +- locals and globals display +- watch expressions with autocompletion +- variable inspection tooltip on hover +- set variable value in locals, watch and hover windows +- call stack display and navigation +- hierarchical variable value display popup (see + 'VimspectorBalloonEval') +- interactive debug console with autocompletion +- launch debuggee within Vim's embedded terminal +- logging/stdout display +- simple stable API for custom tooling (e.g. integrate with language server) + +------------------------------------------------------------------------------- + *vimspector-supported-languages* +Supported languages ~ + +The following table lists the languages that are "built-in" (along with their +runtime dependencies). They are categorised by their level of support: + +- 'Tested' : Fully supported, Vimspector regression tests cover them +- 'Supported' : Fully supported, frequently used and manually tested +- 'Experimental': Working, but not frequently used and rarely tested +- 'Legacy': No longer supported, please migrate your config + +======================================================================================================================================================== +| _Language_ | _Status_ | _Switch (for 'install_gadget.py')_ | _Adapter (for ':VimspectorInstall')_ | _Dependencies_ | +======================================================================================================================================================== +| C, C++, Rust etc. | Tested | '--all' or '--enable-c' (or cpp) | vscode-cpptools | mono-core | +-------------------------------------------------------------------------------------------------------------------------------------------------------- +| Rust, C, C++, etc. | Supported | '--force-enable-rust' | CodeLLDB | Python 3 | +-------------------------------------------------------------------------------------------------------------------------------------------------------- +| Python | Tested | '--all' or '--enable-python' | debugpy | Python 2.7 or Python 3 | +-------------------------------------------------------------------------------------------------------------------------------------------------------- +| Go | Tested | '--enable-go' | vscode-go | Go, Delve [11] | +-------------------------------------------------------------------------------------------------------------------------------------------------------- +| TCL | Supported | '--all' or '--enable-tcl' | tclpro | TCL 8.5 | +-------------------------------------------------------------------------------------------------------------------------------------------------------- +| Bourne Shell | Supported | '--all' or '--enable-bash' | vscode-bash-debug | Bash v?? | +-------------------------------------------------------------------------------------------------------------------------------------------------------- +| Lua | Supported | '--all' or '--enable-lua' | local-lua-debugger-vscode | Node >=12.13.0, Npm, Lua interpreter | +-------------------------------------------------------------------------------------------------------------------------------------------------------- +| Node.js | Supported | '--force-enable-node' | vscode-node-debug2 | 6 < Node < 12, Npm | +-------------------------------------------------------------------------------------------------------------------------------------------------------- +| Javascript | Supported | '--force-enable-chrome' | debugger-for-chrome | Chrome | +-------------------------------------------------------------------------------------------------------------------------------------------------------- +| Java | Supported | '--force-enable-java' | vscode-java-debug | Compatible LSP plugin (see later) | +-------------------------------------------------------------------------------------------------------------------------------------------------------- +| C# (dotnet core) | Experimental | '--force-enable-csharp' | netcoredbg | DotNet core | +-------------------------------------------------------------------------------------------------------------------------------------------------------- +| C# (mono) | Experimental | '--force-enable-csharp' | vscode-mono-debug | Mono | +-------------------------------------------------------------------------------------------------------------------------------------------------------- +| F#, VB, etc. | Experimental | '--force-enable-fsharp' (or vbnet) | netcoredbg | DotNet core | +-------------------------------------------------------------------------------------------------------------------------------------------------------- +| Python.legacy | Legacy | '--force-enable-python.legacy' | vscode-python | Node 10, Python 2.7 or Python 3 | +-------------------------------------------------------------------------------------------------------------------------------------------------------- + + +------------------------------------------------------------------------------- + *vimspector-other-languages* +Other languages ~ + +Vimspector should work for any debug adapter that works in Visual Studio Code. + +To use Vimspector with a language that's not "built-in", see this wiki page +[12]. + +=============================================================================== + *vimspector-installation* +Installation ~ + +------------------------------------------------------------------------------- + *vimspector-quick-start* +Quick Start ~ + +There are 2 installation methods: + +- Using a release tarball and vim packages +- Using a clone of the repo (e.g. package manager) + +Release tarballs come with debug adapters for the default languages +pre-packaged. To use a release tarball: + +1. Check the dependencies +2. Untar the release tarball for your OS into '$HOME/.vim/pack': +> + $ mkdir -p $HOME/.vim/pack + $ curl -L | tar -C $HOME/.vim/pack zxvf - +< +1. Add 'packadd! vimspector' to you '.vimrc' + +2. (optionally) Enable the default set of mappings: +> + let g:vimspector_enable_mappings = 'HUMAN' +< +1. Configure your project's debug profiles (create '.vimspector.json') + +Alternatively, you can clone the repo and select which gadgets are installed: + +1. Check the dependencies +2. Install the plugin as a Vim package. See ':help packages'. +3. Add 'packadd! vimspector' to you '.vimrc' +4. Install some 'gadgets' (debug adapters) - see ':VimspectorInstall ...' +5. Configure your project's debug profiles (create '.vimspector.json') + +If you prefer to use a plugin manager, see the plugin manager's docs. For +Vundle, use: +> + Plugin 'puremourning/vimspector' +< +The following sections expand on the above brief overview. + +------------------------------------------------------------------------------- + *vimspector-dependencies* +Dependencies ~ + +Vimspector requires: + +- One of: +- Vim 8.2 Huge build compiled with Python 3.6 or later +- Neovim 0.4.3 with Python 3.6 or later (experimental) +- One of the following operating systems: +- Linux +- macOS Mojave or later +- Windows (experimental) + +Why such a new vim ? Well 2 reasons: + +1. Because vimspector uses a lot of new Vim features +2. Because there are Vim bugs that vimspector triggers that will frustrate + you if you hit them. + +Why is neovim experimental? Because the author doesn't use neovim regularly, +and there are no regression tests for vimspector in neovim, so it may break +occasionally. Issue reports are handled on best-efforts basis, and PRs are +welcome to fix bugs. See also the next section descibing differences for neovim +vs vim. + +Why Windows support experimental? Because it's effort and it's not a priority +for the author. PRs are welcome to fix bugs. Windows will not be regularly +tested. + +Which Linux versions? I only test on Ubuntu 18.04 and later and RHEL 7. + +------------------------------------------------------------------------------- + *vimspector-neovim-differences* +Neovim differences ~ + +neovim doesn't implement some features Vimspector relies on: + +- WinBar - used for the buttons at the top of the code window and for + changing the output window's current output. + +- Prompt Buffers - used to send commands in the Console and add Watches. + (_Note_: prompt buffers are available in neovim nightly) + +- Balloons - this allows for the variable evaluation popup to be displayed + when hovering the mouse. See below for how to create a keyboard mapping + instead. + +Workarounds are in place as follows: + +- WinBar - There are mappings, ':VimspectorShowOutput' and ':VimspectorReset' + +- Prompt Buffers - There are ':VimspectorEval' and ':VimspectorWatch' + +- Balloons - There is the 'VimspectorBalloonEval' mapping. There is no + default mapping for this, so I recommend something like this to get + variable display in a popup: +> + " mnemonic 'di' = 'debug inspect' (pick your own, if you prefer!) + + " for normal mode - the word under the cursor + nmap di VimspectorBalloonEval + " for visual mode, the visually selected text + xmap di VimspectorBalloonEval +< +------------------------------------------------------------------------------- + *vimspector-windows-differences* +Windows differences ~ + +The following features are not implemented for Windows: + +- Tailing the vimspector log in the Output Window. + +------------------------------------------------------------------------------- + *vimspector-trying-it-out* +Trying it out ~ + +If you just want to try out vimspector without changing your vim config, there +are example projects for a number of languages in 'support/test', including: + +- Python ('support/test/python/simple_python') +- Go ('support/test/go/hello_world') +- Nodejs ('support/test/node/simple') +- Chrome ('support/test/chrome/') +- etc. + +To test one of these out, cd to the directory and run: +> + vim -Nu /path/to/vimspector/tests/vimrc --cmd "let g:vimspector_enable_mappings='HUMAN'" +< +Then press ''. + +There's also a C++ project in 'tests/testdata/cpp/simple/' with a 'Makefile' +which can be used to check everything is working. This is used by the +regression tests in CI so should always work, and is a good way to check if the +problem is your configuration rather than a bug. + +------------------------------------------------------------------------------- + *vimspector-cloning-plugin* +Cloning the plugin ~ + +If you're not using a release tarball, you'll need to clone this repo to the +appropriate place. + +1. Clone the plugin + +There are many Vim plugin managers, and I'm not going to state a particular +preference, so if you choose to use one, follow the plugin manager's +documentation. For example, for Vundle, use: +> + Plugin 'puremourning/vimspector' +< +If you don't use a plugin manager already, install vimspector as a Vim package +by cloning this repository into your package path, like this: +> + $ git clone https://github.com/puremourning/vimspector ~/.vim/pack/vimspector/opt/vimspector +< +1. Configure vimspector in your '.vimrc', for example to enable the standard + mapings: +> + let g:vimspector_enable_mappings = 'HUMAN' +< +1. Load vimspector at runtime. This can also be added to your '.vimrc' after + configuring vimspector: +> + packadd! vimspector +< +See support/doc/example_vimrc.vim for a minimal example. + +------------------------------------------------------------------------------- + *vimspector-install-gadgets* +Install some gadgets ~ + +Vimspector is a generic client for Debug Adapters. Debug Adapters (referred to +as 'gadgets' or 'adapters') are what actually do the work of talking to the +real debuggers. + +In order for Vimspector to be useful, you need to have some adapters installed. + +There are a few ways to do this: + +- If you downloaded a tarball, gadgets for main supported languages are + already installed for you. + +- Using ':VimspectorInstall ' (use TAB 'wildmenu' to see + the options, also accepts any 'install_gadget.py' option) + +- Using 'python3 install_gadget.py ' (use '--help' to see all options) + +- Attempting to launch a debug configuration; if the configured adapter can't + be found, vimspector will suggest installing one. + +- Using ':VimspectorUpdate' to install the latest supported versions of the + gadgets. + +Here's a demo of doing some installs and an upgrade: + + Image: asciicast [13] + +Both 'install_gadget.py' and ':VimspectorInstall' do the same set of things, +though the default behaviours are slightly different. For supported languages, +they will: + +- Download the relevant debug adapter at a version that's been tested from + the internet, either as a 'vsix' (Visusal Studio plugin), or clone from + GitHub. If you're in a corporate environment and this is a problem, you may + need to install the gadgets manually. + +- Perform any necessary post-installation actions, such as: + +- Building any binary components + +- Ensuring scripts are executable, because the VSIX packages are usually + broken in this regard. + +- Set up the 'gadgetDir' symlinks for the platform. + +For example, to install the tested debug adapter for a language, run: + +======================================================================================================================================== +| _To install_ | _Script_ | _Command_ | +======================================================================================================================================== +| '' | ':VimspectorInstall ' | +----------------------------------------------------------------------------------------- +| '', '', ... | ':VimspectorInstall ...' | +----------------------------------------------------------------------------------------- +| '' | './install_gadget.py --enable- ...' | ':VimspectorInstall --enable- ...' | +---------------------------------------------------------------------------------------------------------------------------------------- +| Supported adapters | './install_gadget.py --all' | ':VimspectorInstall --all' | +---------------------------------------------------------------------------------------------------------------------------------------- +| Supported adapters, but not TCL | './install_gadget.py --all --disable-tcl' | ':VimspectorInstall --all --disable-tcl' | +---------------------------------------------------------------------------------------------------------------------------------------- +| Supported and experimental adapters | './install_gadget.py --all --force-all' | ':VimspectorInstall --all' | +---------------------------------------------------------------------------------------------------------------------------------------- +| Adapter for specific debug config | Suggested by Vimspector when starting debugging | +----------------------------------------------------------------------------------------- + + +------------------------------------------------------------------------------- + *vimspectorinstall-vimspectorupdate-commands* +VimspectorInstall and VimspectorUpdate commands ~ + +':VimspectorInstall' runs 'install_gadget.py' in the background with some of +the options defaulted. + +':VimspectorUpdate' runs 'install_gadget.py' to re-install (i.e. update) any +gadgets already installed in your '.gadgets.json'. + +The output is minimal, to see the full output add '--verbose' to the command, +as in ':VimspectorInstall --verbose ...' or ':VimspectorUpdate --verbose ...'. + +If the installation is successful, the output window is closed (and the output +lost forever). Use a '!' to keep it open (e.g. ':VimspectorInstall! --verbose +--all' or ':VimspectorUpdate!' (etc.). + +If you know in advance which gadgets you want to install, for example so that +you can reproduce your config from source control, you can set +'g:vimspector_install_gadgets' to a list of gadgets. This will be used when: + +- Running ':VimspectorInstall' with no arguments, or +- Running ':VimspectorUpdate' + +For example: +> + let g:vimspector_install_gadgets = [ 'debugpy', 'vscode-cpptools', 'CodeLLDB' ] +< +------------------------------------------------------------------------------- + *vimspector-install_gadget.py* +install_gadget.py ~ + +By default 'install_gadget.py' will overwrite your '.gadgets.json' with the set +of adapters just installed, whereas ':VimspectorInstall' will _update_ it, +overwriting only newly changed or installed adapters. + +If you want to just add a new adapter using the script without destroying the +existing ones, add '--update-gadget-config', as in: +> + $ ./install_gadget.py --enable-tcl + $ ./install_gadget.py --enable-rust --update-gadget-config + $ ./install_gadget.py --enable-java --update-gadget-config +< +If you want to maintain 'configurations' outside of the vimspector repository +(this can be useful if you have custom gadgets or global configurations), you +can tell the installer to use a different basedir, then set +'g:vimspector_base_dir' to point to that directory, for example: +> + $ ./install_gadget.py --basedir $HOME/.vim/vimspector-config --all --force-all +< +Then add this to your '.vimrc': +> + let g:vimspector_base_dir=expand( '$HOME/.vim/vimspector-config' ) +< +When usnig ':VimspectorInstall', the 'g:vimspector_base_dir' setting is +respected unless '--basedir' is manually added (not recommended). + +See '--help' for more info on the various options. + +------------------------------------------------------------------------------- + *vimspector-manual-gadget-installation* +Manual gadget installation ~ + +If the language you want to debug is not in the supported list above, you can +probably still make it work, but it's more effort. + +You essentially need to get a working installation of the debug adapter, find +out how to start it, and configure that in an 'adapters' entry in either your +'.vimspector.json' or in '.gadgets.json'. + +The simplest way in practice is to install or start Visual Studio Code and use +its extension manager to install the relevant extension. You can then configure +the adapter manually in the 'adapters' section of your '.vimspector.json' or in +a 'gadgets.json'. + +PRs are always welcome to add supported languages (which roughly translates to +updating 'python/vimspector/gadgets.py' and testing it). + +------------------------------------------------------------------------------- + *vimspector-gadget-directory* +The gadget directory ~ + +Vimspector uses the following directory by default to look for a file named +'.gadgets.json': '/gadgets/'. + +This path is exposed as the vimspector _variable_ '${gadgetDir}'. This is +useful for configuring gadget command lines. + +Where os is one of: + +- 'macos' +- 'linux' +- 'windows' (though note: Windows is not supported) + +The format is the same as '.vimspector.json', but only the 'adapters' key is +used: + +Example: +> + { + "adapters": { + "lldb-vscode": { + "variables": { + "LLVM": { + "shell": "brew --prefix llvm" + } + }, + "attach": { + "pidProperty": "pid", + "pidSelect": "ask" + }, + "command": [ + "${LLVM}/bin/lldb-vscode" + ], + "env": { + "LLDB_LAUNCH_FLAG_LAUNCH_IN_TTY": "YES" + }, + "name": "lldb" + }, + "vscode-cpptools": { + "attach": { + "pidProperty": "processId", + "pidSelect": "ask" + }, + "command": [ + "${gadgetDir}/vscode-cpptools/debugAdapters/OpenDebugAD7" + ], + "name": "cppdbg" + }, + "vscode-python": { + "command": [ + "node", + "${gadgetDir}/vscode-python/out/client/debugger/debugAdapter/main.js" + ], + "name": "vscode-python" + } + } + } +< +The gadget file is automatically written by 'install_gadget.py' (or +':VimspectorInstall'). + +Vimspector will also load any fies matching: +'/gadgets//.gadgets.d/*.json'. These have the same +format as '.gadgets.json' but are not overwritten when running +'install_gadget.py'. + +------------------------------------------------------------------------------- + *vimspector-upgrade* +Upgrade ~ + +After updating the Vimspector code (either via 'git pull' or whatever package +manager), run ':VimspectorUpdate' to update any already-installed gadgets. + +=============================================================================== + *vimspector-about* +About ~ + +------------------------------------------------------------------------------- + *vimspector-background* +Background ~ + +The motivation is that debugging in Vim is a pretty horrible experience, +particularly if you use multiple languages. With pyclewn no more and the +built-in termdebug plugin limited to gdb, I wanted to explore options. + +While Language Server Protocol is well known, the Debug Adapter Protocol is +less well known, but achieves a similar goal: language agnostic API abstracting +debuggers from clients. + +The aim of this project is to provide a simple but effective debugging +experience in Vim for multiple languages, by leveraging the debug adapters that +are being built for Visual Studio Code. + +The ability to do remote debugging is a must. This is key to my workflow, so +baking it in to the debugging experience is a top bill goal for the project. So +vimspector has first-class support for executing programs remotely and +attaching to them. This support is unique to vimspector and on top of +(complementary to) any such support in actual debug adapters. + +------------------------------------------------------------------------------- + *vimspector-status* +Status ~ + +Vimspector is a work in progress, and any feedback/contributions are more than +welcome. + +The backlog can be viewed on Trello [15]. + +------------------------------------------------------------------------------- + *vimspector-experimental* +Experimental ~ + +The plugin is currently _experimental_. That means that any part of it can (and +probably will) change, including things like: + +- breaking changes to the configuration +- keys, layout, functionality of the UI + +However, I commit to only doing this in the most extreme cases and to annouce +such changes on Gitter well in advance. There's nothing more annoying than +stuff just breaking on you. I get that. + +------------------------------------------------------------------------------- + *vimspector-motivation* +Motivation ~ + +A message from the author about the motivation for this plugin: + +Many development environments have a built-in debugger. I spend an inordinate +amount of my time in Vim. I do all my development in Vim and I have even +customised my workflows for building code, running tests etc. + +For many years I have observed myself, friends and colleagues have been writing +'printf', 'puts', 'print', etc. debugging statements in all sorts of files +simply because there is no _easy_ way to run a debugger for _whatever_ language +we happen to be developing in. + +I truly believe that interactive, graphical debugging environments are the best +way to understand and reason about both unfamiliar and familiar code, and that +the lack of ready, simple access to a debugger is a huge hidden productivity +hole for many. + +Don't get me wrong, I know there are literally millions of developers out there +that are more than competent at developing without a graphical debugger, but I +maintain that if they had the ability to _just press a key_ and jump into the +debugger, it would be faster and more enjoyable that just cerebral code +comprehension. + +I created Vimspector because I find changing tools frustrating. 'gdb' for c++, +'pdb' for python, etc. Each has its own syntax. Each its own lexicon. Each its +own foibles. + +I designed the configuration system in such a way that the configuration can be +committed to source control so that it _just works_ for any of your colleagues, +friends, collaborators or complete strangers. + +I made remote debugging a first-class feature because that's a primary use case +for me in my job. + +With Vimspector I can _just hit ''_ in all of the languages I develop in +and debug locally or remotely using the exact same workflow, mappings and UI. I +have integrated this with my Vim in such a way that I can hit a button and _run +the test under the cursor in Vimspector_. This kind of integration has +massively improved my workflow and productivity. It's even made the process of +learning a new codebase... fun. + +- Ben Jackson, Creator. + +------------------------------------------------------------------------------- + *vimspector-license* +License ~ + +Apache 2.0 [16] + +Copyright © 2018 Ben Jackson + +------------------------------------------------------------------------------- + *vimspector-sponsorship* +Sponsorship ~ + +If you like Vimspector so much that you're wiling to part with your hard-earned +cash, please consider donating to one of the following charities, which are +meaningful to the author of Vimspector (in order of preference): + +- Greyhound Rescue Wales [17] +- Cancer Research UK [18] +- ICCF Holland [19] +- Any charity of your choosing. + +=============================================================================== + *vimspector-mappings* +Mappings ~ + +By default, vimspector does not change any of your mappings. Mappings are very +personal and so you should work out what you like and use vim's powerful +mapping features to set your own mappings. To that end, Vimspector defines the +following '' mappings: + +================================================================================================================================================================================= +| _Mapping_ | _Function_ | _API_ | +================================================================================================================================================================================= +| 'VimspectorContinue' | When debugging, continue. Otherwise start debugging. | 'vimspector#Continue()' | +--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +| 'VimspectorStop' | Stop debugging. | 'vimspector#Stop()' | +--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +| 'VimpectorRestart' | Restart debugging with the same configuration. | 'vimspector#Restart()' | +--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +| 'VimspectorPause' | Pause debuggee. | 'vimspector#Pause()' | +--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +| 'VimspectorToggleBreakpoint' | Toggle line breakpoint on the current line. | 'vimspector#ToggleBreakpoint()' | +--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +| 'VimspectorToggleConditionalBreakpoint' | Toggle conditional line breakpoint on the current line. | 'vimspector#ToggleBreakpoint( { trigger expr, hit count expr } )' | +--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +| 'VimspectorAddFunctionBreakpoint' | Add a function breakpoint for the expression under cursor | "vimspector#AddFunctionBreakpoint( '' )" | +--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +| 'VimspectorRunToCursor' | Run to Cursor | 'vimspector#RunToCursor()' | +--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +| 'VimspectorStepOver' | Step Over | 'vimspector#StepOver()' | +--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +| 'VimspectorStepInto' | Step Into | 'vimspector#StepInto()' | +--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +| 'VimspectorStepOut' | Step out of current function scope | 'vimspector#StepOut()' | +--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +| 'VimspectorUpFrame' | Move up a frame in the current call stack | 'vimspector#UpFrame()' | +--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +| 'VimspectorDownFrame' | Move down a frame in the current call stack | 'vimspector#DownFrame()' | +--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +| 'VimspectorBalloonEval' | Evaluate expression under cursor (or visual) in popup | _internal_ | +--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + + +These map roughly 1-1 with the API functions below. + +For example, if you want '' to start/continue debugging, add this to some +appropriate place, such as your 'vimrc' (hint: run ':e $MYVIMRC'). +> + nmap VimspectorContinue +< +In addition, many users probably want to only enable certain Vimspector +mappings while debugging is active. This is also possible, though it requires +writing some vimscipt. + +That said, many people are familiar with particular debuggers, so the following +mappings can be enabled by setting 'g:vimspector_enable_mappings' to the +specified value. + +------------------------------------------------------------------------------- + *vimspector-visual-studio-vscode* +Visual Studio / VSCode ~ + +To use Visual Studio-like mappings, add the following to your 'vimrc' **before +loading vimspector**: +> + let g:vimspector_enable_mappings = 'VISUAL_STUDIO' +< +========================================================================================================================= +| _Key_ | _Mapping_ | _Function_ | +========================================================================================================================= +| 'F5' | 'VimspectorContinue' | When debugging, continue. Otherwise start debugging. | +------------------------------------------------------------------------------------------------------------------------- +| 'Shift F5' | 'VimspectorStop' | Stop debugging. | +------------------------------------------------------------------------------------------------------------------------- +| 'Ctrl Shift F5' | 'VimspectorRestart' | Restart debugging with the same configuration. | +------------------------------------------------------------------------------------------------------------------------- +| 'F6' | 'VimspectorPause' | Pause debuggee. | +------------------------------------------------------------------------------------------------------------------------- +| 'F9' | 'VimspectorToggleBreakpoint' | Toggle line breakpoint on the current line. | +------------------------------------------------------------------------------------------------------------------------- +| 'Shift F9' | 'VimspectorAddFunctionBreakpoint' | Add a function breakpoint for the expression under cursor | +------------------------------------------------------------------------------------------------------------------------- +| 'F10' | 'VimspectorStepOver' | Step Over | +------------------------------------------------------------------------------------------------------------------------- +| 'F11' | 'VimspectorStepInto' | Step Into | +------------------------------------------------------------------------------------------------------------------------- +| 'Shift F11' | 'VimspectorStepOut' | Step out of current function scope | +------------------------------------------------------------------------------------------------------------------------- + + +------------------------------------------------------------------------------- + *vimspector-human-mode* +Human Mode ~ + +If, like me, you only have 2 hands and 10 fingers, you probably don't like +Ctrl-Shift-F keys. Also, if you're running in a terminal, there's a real +possibility of terminfo being wrong for shifted-F-keys, particularly if your +'TERM' is 'screen-256color'. If these issues (number of hands, 'TERM' +variables) are unfixable, try the following mappings, by adding the following +**before loading vimspector**: +> + let g:vimspector_enable_mappings = 'HUMAN' +< +============================================================================================================================ +| _Key_ | _Mapping_ | _Function_ | +============================================================================================================================ +| 'F5' | 'VimspectorContinue' | When debugging, continue. Otherwise start debugging. | +---------------------------------------------------------------------------------------------------------------------------- +| 'F3' | 'VimspectorStop' | Stop debugging. | +---------------------------------------------------------------------------------------------------------------------------- +| 'F4' | 'VimspectorRestart' | Restart debugging with the same configuration. | +---------------------------------------------------------------------------------------------------------------------------- +| 'F6' | 'VimspectorPause' | Pause debuggee. | +---------------------------------------------------------------------------------------------------------------------------- +| 'F9' | 'VimspectorToggleBreakpoint' | Toggle line breakpoint on the current line. | +---------------------------------------------------------------------------------------------------------------------------- +| 'F9' | 'VimspectorToggleConditionalBreakpoint' | Toggle conditional line breakpoint on the current line. | +---------------------------------------------------------------------------------------------------------------------------- +| 'F8' | 'VimspectorAddFunctionBreakpoint' | Add a function breakpoint for the expression under cursor | +---------------------------------------------------------------------------------------------------------------------------- +| 'F8' | 'VimspectorRunToCursor' | Run to Cursor | +---------------------------------------------------------------------------------------------------------------------------- +| 'F10' | 'VimspectorStepOver' | Step Over | +---------------------------------------------------------------------------------------------------------------------------- +| 'F11' | 'VimspectorStepInto' | Step Into | +---------------------------------------------------------------------------------------------------------------------------- +| 'F12' | 'VimspectorStepOut' | Step out of current function scope | +---------------------------------------------------------------------------------------------------------------------------- + + +In addition, I recommend adding a mapping to 'VimspectorBalloonEval', in +normal and visual modes, for example: +> + " mnemonic 'di' = 'debug inspect' (pick your own, if you prefer!) + + " for normal mode - the word under the cursor + nmap di VimspectorBalloonEval + " for visual mode, the visually selected text + xmap di VimspectorBalloonEval +< +You may also wish to add mappings for up/down the stack, for example: +> + nmap VimspectorUpFrame + nmap VimspectorDownFrame +< +=============================================================================== + *vimspector-usage-api* +Usage and API ~ + +This section defines detailed usage instructions, organised by feature. For +most users, the mappings section contains the most common commands and default +usage. This section can be used as a reference to create your own mappings or +custom behaviours. + +------------------------------------------------------------------------------- + *vimspector-launch-attach-by-pid* +Launch and attach by PID: ~ + +- Create '.vimspector.json'. See below. +- ':call vimspector#Launch()' and select a configuration. + + Image: debug session (see reference [6]) + +------------------------------------------------------------------------------- + *vimspector-launch-with-options* +Launch with options ~ + +To launch a specific debug configuration, or specify replacement variables [20] +for the launch, you can use: + +- ':call vimspector#LaunchWithSettings( dict )' + +The argument is a 'dict' with the following keys: + +- 'configuration': (optional) Name of the debug configuration to launch +- '': (optional) Name of a variable to set + +This allows for some integration and automation. For example, if you have a +configuration named 'Run Test' that contains a replacement variable [20] named +'${Test}' you could write a mapping which ultimately executes: +> + vimspector#LaunchWithSettings( #{ configuration: 'Run Test' + \ Test: 'Name of the test' } ) +< +This would start the 'Run Test' configuration with '${Test}' set to "'Name of +the test'" and Vimspector would _not_ prompt the user to enter or confirm these +things. + +See our YouCompleteMe integration guide for another example where it can be +used to specify the port to connect the java debugger + +------------------------------------------------------------------------------- + *vimspector-debug-configuration-selection* +Debug configuration selection ~ + +Vimspector uses the following logic to choose a configuration to launch: + +1. If a configuration was specified in the launch options (as above), use + that. + +2. Otherwise if there's only one configuration and it doesn't have + 'autoselect' set to 'false', use that. + +3. Otherwise if there's exactly one configuration with 'default' set to + 'true' and without 'autoselect' set to 'false', use that. + +4. Otherwise, prompt the user to select a configuration. + +See the reference guide [21] for details. + +------------------------------------------------------------------------------- + *vimspector-get-configurations* +Get configurations ~ + +- Use 'vimspector#GetConfigurations()' to get a list of configurations + +For example, to get an array of configurations and fuzzy matching on the result +> + :call matchfuzzy(vimspector#GetConfigurations(), "test::case_1") +< +------------------------------------------------------------------------------- + *vimspector-breakpoints* +Breakpoints ~ + +See the mappings [22] section for the default mappings for working with +breakpoints. This section describes the full API in vimscript functions. + +------------------------------------------------------------------------------- + *vimspector-summary* +Summary ~ + +- Use 'vimspector#ToggleBreakpoint( { options dict } )' to set/disable/delete + a line breakpoint. The argument is optional (see below). + +- Use "vimspector#AddFunctionBreakpoint( '', { options dict} )" to add + a function breakpoint. The second argument is optional (see below). + +- Use 'vimspector#SetLineBreakpoint( file_name, line_num, { options dict } )' + to set a breakpoint at a specific file/line. The last argument is optional + (see below) + +- Use 'vimspector#ClearLineBreakpoint( file_name, line_num )' to remove a + breakpoint at a specific file/line + +- Use 'vimspector#ClearBreakpoints()' to clear all breakpoints + +Examples: + +- 'call vimspector#ToggleBreakpoint()' - toggle breakpoint on current line + +- "call vimspector#SetLineBreakpoint( 'some_file.py', 10 )" - set a + breakpoint on 'some_filepy:10' + +- "call vimspector#AddFunctionBreakpoint( 'main' )" - add a function + breakpoint on the 'main' function + +- "call vimspector#ToggleBreakpoint( { 'condition': 'i > 5' } )" - add a + breakpoint on the current line that triggers only when 'i > 5' is 'true' + +- "call vimspector#SetLineBreakpoint( 'some_file.py', 10, { 'condition': 'i > + 5' } )" - add a breakpoint at 'some_file.py:10' that triggers only when 'i + > 5' is 'true' + +- "call vimspector#ClearLineBreakpoint( 'some_file.py', 10 )" - delete the + breakpoint at 'some_file.py:10' + +- 'call vimspector#ClearBreakpoints()' - clear all breakpoints + +------------------------------------------------------------------------------- + *vimspector-line-breakpoints* +Line breakpoints ~ + +The simplest and most common form of breakpoint is a line breakpoint. Execution +is paused when the specified line is executed. + +For most debugging scenarios, users will just hit '' to create a line +breakpoint on the current line and '' to launch the application. + +------------------------------------------------------------------------------- + *vimspector-conditional-breakpoints* +Conditional breakpoints ~ + +Some debug adapters support conditional breakpoints. Note that vimspector does +not tell you if the debugger doesn't support conditional breakpoints (yet). A +conditional breakpoint is a breakpoint which only triggers if some expression +evaluates to true, or has some other constraints met. + +Some of these functions above take a single optional argument which is a +dictionary of options. The dictionary can have the following keys: + +- 'condition': An optional expression evaluated to determine if the + breakpoint should fire. Not supported by all debug adapters. For example, + to break when 'abc' is '10', enter something like 'abc == 10', depending on + the language. + +- 'hitCondition': An optional expression evaluated to determine a number of + times the breakpoint should be ignored. Should (probably?) not be used in + combination with 'condition'. Not supported by all debug adapters. For + example, to break on the 3rd time hitting this line, enter '3'. + +In both cases, the expression is evaluated by the debugger, so should be in +whatever dialect the debugger understands when evaluating expressions. + +When using the '' mapping, the user is prompted to enter these +expressions in a command line (with history). + +------------------------------------------------------------------------------- + *vimspector-exception-breakpoints* +Exception breakpoints ~ + +Exception breakpoints typically fire when an exception is throw or other error +condition occurs. Depending on the debugger, when starting debugging, you may +be asked a few questions about how to handle exceptions. These are "exception +breakpoints" and vimspector remembers your choices while Vim is still running. + +Typically you can accept the defaults (just keep pressing ''!) as most +debug adapter defaults are sane, but if you want to break on, say 'uncaught +exception' then answer 'Y' to that (for example). + +You can configure your choices in the '.vimspector.json'. See the configuration +guide [23] for details on that. + +------------------------------------------------------------------------------- + *vimspector-clear-breakpoints* +Clear breakpoints ~ + +Use 'vimspector#ClearBreakpoints()' to clear all breakpoints including the +memory of exception breakpoint choices. + +------------------------------------------------------------------------------- + *vimspector-run-to-cursor* +Run to Cursor ~ + +Use 'vimspector#RunToCursor' or '': this creates a temporary +breakpoint on the current line, then continues execution, clearing the +breakpoint when it is hit. + +------------------------------------------------------------------------------- + *vimspector-stepping* +Stepping ~ + +- Step in/out, finish, continue, pause etc. using the WinBar, or mappings. +- If you really want to, the API is 'vimspector#StepInto()' etc. + + Image: code window (see reference [24]) + +------------------------------------------------------------------------------- + *vimspector-variables-scopes* +Variables and scopes ~ + +- Current scope shows values of locals. +- Use '', or double-click with left mouse to expand/collapse (+, -). +- Set the value of the variable with '' (control + '') or + '' (if 'modifyOtherKeys' doesn't work for you) +- When changing the stack frame the locals window updates. +- While paused, hover to see values + + Image: locals window (see reference [25]) + +Scopes and variables are represented by the buffer 'vimspector.Variables'. + +------------------------------------------------------------------------------- + *vimspector-variable-or-selection-hover-evaluation* +Variable or selection hover evaluation ~ + +All rules for 'Variables and scopes' apply plus the following: + +- With mouse enabled, hover over a variable and get the value it evaluates + to. + +- Use your mouse to perform a visual selection of an expression (e.g. 'a + + b') and get its result. + +- Make a normal mode ('nmap') and visual mode ('xmap') mapping to + 'VimspectorBalloonEval' to manually trigger the popup. + +- Set the value of the variable with '' (control + '') or + '' (if 'modifyOtherKeys' doesn't work for you) + +- Use regular nagivation keys ('j', 'k') to choose the current selection; + '' (or leave the tooltip window) to close the tooltip. + + Image: variable eval hover (see reference [26]) + +------------------------------------------------------------------------------- + *vimspector-watches* +Watches ~ + +The watch window is used to inspect variables and expressions. Expressions are +evaluated in the selected stack frame which is "focussed" + +The watches window is a prompt buffer, where that's available. Enter insert +mode to add a new watch expression. + +- Add watches to the variables window by entering insert mode and typing the + expression. Commit with ''. + +- Alternatively, use ':VimspectorWatch '. Tab-completion for + expression is available in some debug adapters. + +- Expand result with '', or double-click with left mouse. + +- Set the value of the variable with '' (control + '') or + '' (if 'modifyOtherKeys' doesn't work for you) + +- Delete with ''. + + Image: watch window (see reference [27]) + +The watches are represented by the buffer 'vimspector.StackTrace'. + +------------------------------------------------------------------------------- + *vimspector-watch-autocompletion* +Watch autocompletion ~ + +The watch prompt buffer has its 'omnifunc' set to a function that will +calculate completion for the current expression. This is trivially used with +'' (see ':help ins-completion'), or integrated with your +favourite completion system. The filetype in the buffer is set to +'VimspectorPrompt'. + +For YouCompleteMe, the following config works well: +> + let g:ycm_semantic_triggers = { + \ 'VimspectorPrompt': [ '.', '->', ':', '<' ] + } +< +------------------------------------------------------------------------------- + *vimspector-stack-traces* +Stack Traces ~ + +The stack trace window shows the state of each program thread. Threads which +are stopped can be expanded to show the stack trace of that thread. + +Often, but not always, all threads are stopped when a breakpoint is hit. The +status of a thread is show in parentheses after the thread's name. Where +supported by the underlying debugger, threads can be paused and continued +individually from within the Stack Trace window. + +A particular thread, highlighted with the 'CursorLine' highlight group is the +"focussed" thread. This is the thread that receives commands like "Stop In", +"Stop Out", "Continue" and "Pause" in the code window. The focussed thread can +be changed manually to "switch to" that thread. + +- Use '', or double-click with left mouse to expand/collapse a thread + stack trace, or use the WinBar button. + +- Use '', or double-click with left mouse on a stack frame to jump to it. + +- Use the WinBar or 'vimspector#PauseContinueThread()' to individually pause + or continue the selected thread. + +- Use the "Focus" WinBar button, '' or + 'vimspector#SetCurrentThread()' to set the "focussed" thread to the + currently selected one. If the selected line is a stack frame, set the + focussed thread to the thread of that frame and jump to that frame in the + code window. + + Image: stack trace (see reference [28]) + +The stack trace is represented by the buffer 'vimspector.StackTrace'. + +------------------------------------------------------------------------------- + *vimspector-program-output* +Program Output ~ + +- In the outputs window, use the WinBar to select the output channel. +- Alternatively, use ':VimspectorShowOutput '. Use command-line + completion to see the categories. +- The debuggee prints to the stdout channel. +- Other channels may be useful for debugging. + + Image: output window (see reference [29]) + +If the output window is closed, a new one can be opened with +':VimspectorShowOutput ' (use tab-completion - 'wildmenu' to see the +options). + +------------------------------------------------------------------------------- + *vimspector-console* +Console ~ + +The console window is a prompt buffer, where that's available, and can be used +as an interactive CLI for the debug adapter. Support for this varies amongst +adapters. + +- Enter insert mode to enter a command to evaluate. +- Alternatively, ':VimspectorEval '. Completion is available with + some debug adapters. +- Commit the request with '' +- The request and subsequent result are printed. + +NOTE: See also Watches above. + +If the output window is closed, a new one can be opened with +':VimspectorShowOutput Console'. + +------------------------------------------------------------------------------- + *vimspector-console-autocompletion* +Console autocompletion ~ + +The console prompt buffer has its 'omnifunc' set to a function that will +calculate completion for the current command/expression. This is trivially used +with '' (see ':help ins-completion'), or integrated with your +favourite completion system. The filetype in the buffer is set to +'VimspectorPrompt'. + +For YouCompleteMe, the following config works well: +> + let g:ycm_semantic_triggers = { + \ 'VimspectorPrompt': [ '.', '->', ':', '<' ] + } +< +------------------------------------------------------------------------------- + *vimspector-log-view* +Log View ~ + +The Vimspector log file contains a full trace of the communication between +Vimspector and the debug adapter. This is the primary source of diagnostic +information when something goes wrong that's not a Vim traceback. + +If you just want to see the Vimspector log file, use ':VimspectorToggleLog', +which will tail it in a little window (doesn't work on Windows). + +------------------------------------------------------------------------------- + *vimspector-closing-debugger* +Closing debugger ~ + +To close the debugger, use: + +- 'Reset' WinBar button +- ':VimspectorReset' when the WinBar is not available. +- 'call vimspector#Reset()' + +------------------------------------------------------------------------------- + *vimspector-terminate-debuggee* +Terminate debuggee ~ + +If the debuggee is still running when stopping or resetting, then some debug +adapters allow you to specify what should happen to it when finishing +debugging. Typically, the default behaviour is sensible, and this is what +happens most of the time. These are the defaults according to DAP: + +- If the request was 'launch': terminate the debuggee +- If the request was 'attach': don't terminate the debuggee + +Some debug adapters allow you to choose what to do when disconnecting. If you +wish to control this behaviour, use ':VimspectorReset' or call +"vimspector#Reset( { 'interactive': v:true } )". If the debug adapter offers a +choice as to whether or not to terminate the debuggee, you will be prompted to +choose. The same applies for 'vimspector#Stop()' which can take an argument: +"vimspector#Stop( { 'interactive': v:true } )". + +=============================================================================== + *vimspector-debug-profile-configuration* +Debug profile configuration ~ + +For an introduction to the configuration of '.vimspector.json', take a look at +the Getting Started section of the Vimspector website [1]. + +For full explanation, including how to use variables, substitutions and how to +specify exception breakpoints, see the docs [2]. + +The JSON configuration file allows C-style comments: + +- '// comment to end of line ...' +- '/* inline comment ... */' + +Current tested with the following debug adapters. + +------------------------------------------------------------------------------- + *vimspector-c-c-rust-etc.* +C, C++, Rust, etc. ~ + +- vscode-cpptools [30] + +- On macOS, I _strongly_ recommend using CodeLLDB instead for C and C++ + projects. It's really excellent, has fewer dependencies and doesn't open + console apps in another Terminal window. + +Example '.vimspector.json' (works with both 'vscode-cpptools' and +'lldb-vscode'. For 'lldb-vscode' replace the name of the adapter with +'lldb-vscode': + +- vscode-cpptools Linux/MacOS: +> + { + "configurations": { + "Launch": { + "adapter": "vscode-cpptools", + "configuration": { + "request": "launch", + "program": "", + "args": [ ... ], + "cwd": "", + "environment": [ ... ], + "externalConsole": true, + "MIMode": "" + } + }, + "Attach": { + "adapter": "vscode-cpptools", + "configuration": { + "request": "attach", + "program": "", + "MIMode": "" + } + } + ... + } + } +< +- vscode-cpptools Windows + +**_NOTE FOR WINDOWS USERS:_** You need to install 'gdb.exe'. I recommend using +'scoop install gdb'. Vimspector cannot use the visual studio debugger due to +licensing. +> + { + "configurations": { + "Launch": { + "adapter": "vscode-cpptools", + "configuration": { + "request": "launch", + "program": "", + "stopAtEntry": true + } + } + } + } +< +------------------------------------------------------------------------------- + *vimspector-c-remote-debugging* +C++ Remote debugging ~ + +The cpptools documentation describes how to attach cpptools to gdbserver using +'miDebuggerAddress'. Note that when doing this you should use the '"request": +"attach"'. + +------------------------------------------------------------------------------- + *vimspector-c-remote-launch-attach* +C++ Remote launch and attach ~ + +If you're feeling fancy, checkout the reference guide [31] for an example of +getting Vimspector to remotely launch and attach. + +- CodeLLDB (MacOS) + +CodeLLDB is superior to vscode-cpptools in a number of ways on macOS at least. + +See Rust. + +- lldb-vscode (MacOS) + +An alternative is to to use 'lldb-vscode', which comes with llvm. Here's how: + +- Install llvm (e.g. with HomeBrew: 'brew install llvm') +- Create a file named + '/path/to/vimspector/gadgets/macos/.gadgets.d/lldb-vscode.json': +> + { + "adapters": { + "lldb-vscode": { + "variables": { + "LLVM": { + "shell": "brew --prefix llvm" + } + }, + "attach": { + "pidProperty": "pid", + "pidSelect": "ask" + }, + "command": [ + "${LLVM}/bin/lldb-vscode" + ], + "env": { + "LLDB_LAUNCH_FLAG_LAUNCH_IN_TTY": "YES" + }, + "name": "lldb" + } + } + } +< +------------------------------------------------------------------------------- + *vimspector-rust* +Rust ~ + +Rust is supported with any gdb/lldb-based debugger. So it works fine with +'vscode-cpptools' and 'lldb-vscode' above. However, support for rust is best in +'CodeLLDB' [32]. + +- './install_gadget.py --force-enable-rust' or ':VimspectorInstall CodeLLDB' +- Example: 'support/test/rust/vimspector_test' +> + { + "configurations": { + "launch": { + "adapter": "CodeLLDB", + "configuration": { + "request": "launch", + "program": "${workspaceRoot}/target/debug/vimspector_test" + } + } + } + } +< +- Docs: https://github.com/vadimcn/vscode-lldb/blob/master/MANUAL.md + +------------------------------------------------------------------------------- + *vimspector-python* +Python ~ + +- Python: debugpy [33] + +- Install with 'install_gadget.py --enable-python' or ':VimspectorInstall + debugpy', ideally requires a working compiler and the python development + headers/libs to build a C python extension for performance. + +- Full options: + https://github.com/microsoft/debugpy/wiki/Debug-configuration-settings + +**Migrating from 'vscode-python'**: change '"adapter": "vscode-python"' to +'"adapter": "debugpy"'. +> + { + "configurations": { + ": Launch": { + "adapter": "debugpy", + "configuration": { + "name": ": Launch", + "type": "python", + "request": "launch", + "cwd": "", + "python": "/path/to/python/interpreter/to/use", + "stopOnEntry": true, + "console": "externalTerminal", + "debugOptions": [], + "program": "" + } + } + ... + } + } +< +------------------------------------------------------------------------------- + *vimspector-python-remote-debugging* +Python Remote Debugging ~ + +In order to use remote debugging with debugpy, you have to connect Vimspector +directly to the application that is being debugged. This is easy, but it's a +little different from how we normally configure things. Specifically, you need +to: + +- Start your application with debugpy, specifying the '--listen' argument. + See the debugpy documentation [34] for details. + +- Use the built-in "multi-session" adapter. This just asks for the host/port + to connect to. For example: +> + { + "configurations": { + "Python Attach": { + "adapter": "multi-session", + "configuration": { + "request": "attach", + "pathMappings": [ + // mappings here (optional) + ] + } + } + } + } +< +See details of the launch configuration [35] for explanation of things like +'pathMappings'. + +Additional documentation, including how to do this when the remote machine can +only be contacted via SSH are provided by debugpy [36]. + +------------------------------------------------------------------------------- + *vimspector-python-remote-launch-attach* +Python Remote launch and attach ~ + +If you're feeling fancy, checkout the reference guide [31] for an example of +getting Vimspector to remotely launch and attach. + +------------------------------------------------------------------------------- + *vimspector-legacy-vscode-python* +Legacy: vscode-python ~ + +- No longer installed by default - please pass '--force-enable-python.legacy' + if you just want to continue using your working setup. +- vscode-python [37] +- NOTE: You must be running 'node' 10. See this issue [38] +> + { + "configurations": { + ": Launch": { + "adapter": "vscode-python", + "configuration": { + "name": ": Launch", + "type": "python", + "request": "launch", + "cwd": "", + "stopOnEntry": true, + "console": "externalTerminal", + "debugOptions": [], + "program": "" + } + } + ... + } + } +< +------------------------------------------------------------------------------- + *vimspector-tcl* +TCL ~ + +- TCL (TclProDebug) + +See my fork of TclProDebug [39] for instructions. + +------------------------------------------------------------------------------- + *vimspector-c* +C♯ ~ + +- C# - dotnet core + +Install with 'install_gadget.py --force-enable-csharp' or ':VimspectorInstall +netcoredbg' +> + { + "configurations": { + "launch - netcoredbg": { + "adapter": "netcoredbg", + "configuration": { + "request": "launch", + "program": "${workspaceRoot}/bin/Debug/netcoreapp2.2/csharp.dll", + "args": [], + "stopAtEntry": true, + "cwd": "${workspaceRoot}" + } + } + } + } +< +- C# - mono + +Install with 'install_gadget.py --force-enable-csharp' or ':VimspectorInstall +vscode-mono-debug'. + +**_Known not to work._** +> + { + "configurations": { + "launch - mono": { + "adapter": "vscode-mono-debug", + "configuration": { + "request": "launch", + "program": "${workspaceRoot}/bin/Debug/netcoreapp2.2/csharp.dll", + "args": [], + "cwd": "${workspaceRoot}", + "runtimeExecutable": "mono", + "runtimeArgs": [], + "env": [], + "externalConsole": false, + "console": "integratedTerminal" + } + } + } + } +< +------------------------------------------------------------------------------- + *vimspector-go* +Go ~ + +- Go + +Requires: + +- 'install_gadget.py --enable-go' or ':VimspectorInstall vscode-go' +- Delve [40] installed, e.g. 'go get -u github.com/go-delve/delve/cmd/dlv' +- Delve to be in your PATH, or specify the 'dlvToolPath' launch option +> + { + "configurations": { + "run": { + "adapter": "vscode-go", + "configuration": { + "request": "launch", + "program": "${fileDirname}", + "mode": "debug", + "dlvToolPath": "$HOME/go/bin/dlv" + } + } + } + } +< +See the vscode-go docs for troubleshooting information [41] + +------------------------------------------------------------------------------- + *vimspector-php* +PHP ~ + +This uses the php-debug, see +https://marketplace.visualstudio.com/items?itemName=felixfbecker.php-debug + +Requires: + +- (optional) Xdebug helper for chrome https://chrome.google.com/webstore/deta + il/xdebug-helper/eadndfjplgieldjbigjakmdgkmoaaaoc + +- 'install_gadget.py --force-enable-php' or ':VimspectorInstall + vscode-php-debug' + +- configured php xdebug extension +> + zend_extension=xdebug.so + xdebug.remote_enable=on + xdebug.remote_handler=dbgp + xdebug.remote_host=localhost + xdebug.remote_port=9000 +< + replace 'localhost' with the ip of your workstation. + +lazy alternative +> + zend_extension=xdebug.so + xdebug.remote_enable=on + xdebug.remote_handler=dbgp + xdebug.remote_connect_back=true + xdebug.remote_port=9000 +< +- .vimspector.json +> + { + "configurations": { + "Listen for XDebug": { + "adapter": "vscode-php-debug", + "configuration": { + "name": "Listen for XDebug", + "type": "php", + "request": "launch", + "port": 9000, + "stopOnEntry": false, + "pathMappings": { + "/var/www/html": "${workspaceRoot}" + } + } + }, + "Launch currently open script": { + "adapter": "vscode-php-debug", + "configuration": { + "name": "Launch currently open script", + "type": "php", + "request": "launch", + "program": "${file}", + "cwd": "${fileDirname}", + "port": 9000 + } + } + } + } +< +------------------------------------------------------------------------------- + *vimspector-debug-web-application* +Debug web application ~ + +append 'XDEBUG_SESSION_START=xdebug' to your query string +> + curl "http://localhost?XDEBUG_SESSION_START=xdebug" +< +or use the previously mentioned Xdebug Helper extension (which sets a +'XDEBUG_SESSION' cookie) + +------------------------------------------------------------------------------- + *vimspector-debug-cli-application* +Debug cli application ~ +> + export XDEBUG_CONFIG="idekey=xdebug" + php +< +------------------------------------------------------------------------------- + *vimspector-javascript-typescript-etc.* +JavaScript, TypeScript, etc. ~ + +- Node.js + +Requires: + +- 'install_gadget.py --force-enable-node' + +- For installation, a Node.js environment that is < node 12. I believe this + is an incompatibility with gulp. Advice, use [nvm][] with 'nvm install + --lts 10; nvm use --lts 10; ./install_gadget.py --force-enable-node ...' + +- Options described here: + https://code.visualstudio.com/docs/nodejs/nodejs-debugging + +- Example: 'support/test/node/simple' +> + { + "configurations": { + "run": { + "adapter": "vscode-node", + "configuration": { + "request": "launch", + "protocol": "auto", + "stopOnEntry": true, + "console": "integratedTerminal", + "program": "${workspaceRoot}/simple.js", + "cwd": "${workspaceRoot}" + } + } + } + } +< +- Chrome + +This uses the chrome debugger, see https://marketplace.visualstudio.com/items?i +temName=msjsdiag.debugger-for-chrome. + +It allows you to debug scripts running inside chrome from within Vim. + +- './install_gadget.py --force-enable-chrome' or ':VimspectorInstall + debugger-for-chrome' +- Example: 'support/test/chrome' +> + { + "configurations": { + "launch": { + "adapter": "chrome", + "configuration": { + "request": "launch", + "url": "http://localhost:1234/", + "webRoot": "${workspaceRoot}/www" + } + } + } + } +< +------------------------------------------------------------------------------- + *vimspector-java* +Java ~ + +Vimspector works well with the java debug server [42], which runs as a jdt.ls +(Java Language Server) plugin, rather than a standalone debug adapter. + +Vimspector is not in the business of running language servers, only debug +adapters, so this means that you need a compatible Language Server Protocol +editor plugin to use Java. I recommend YouCompleteMe [43], which has full +support for jdt.ls, and most importantly a trivial way to load the debug +adapter and to use it with Vimspector. + +------------------------------------------------------------------------------- + *vimspector-usage-with-youcompleteme* +Usage with YouCompleteMe ~ + +- Set up YCM for java [43]. + +- Get Vimspector to download the java debug plugin: 'install_gadget.py + --force-enable-java ' or ':VimspectorInstall + java-debug-adapter' + +- Configure Vimspector for your project using the 'vscode-java' adapter, + e.g.: +> + { + "configurations": { + "Java Attach": { + "adapter": "vscode-java", + "configuration": { + "request": "attach", + "hostName": "${host}", + "port": "${port}", + "sourcePaths": [ + "${workspaceRoot}/src/main/java", + "${workspaceRoot}/src/test/java" + ] + } + } + } + } +< +- Tell YCM to load the debugger plugin. This should be the 'gadgets/' + directory, not any specific adapter. e.g. in '.vimrc' +> + " Tell YCM where to find the plugin. Add to any existing values. + let g:ycm_java_jdtls_extension_path = [ + \ '' + \ ] +< +- Create a mapping, such as '' to start the debug server and + launch vimspector, e.g. in '~/.vim/ftplugin/java.vim': +> + let s:jdt_ls_debugger_port = 0 + function! s:StartDebugging() + if s:jdt_ls_debugger_port <= 0 + " Get the DAP port + let s:jdt_ls_debugger_port = youcompleteme#GetCommandResponse( + \ 'ExecuteCommand', + \ 'vscode.java.startDebugSession' ) + + if s:jdt_ls_debugger_port == '' + echom "Unable to get DAP port - is JDT.LS initialized?" + let s:jdt_ls_debugger_port = 0 + return + endif + endif + + " Start debugging with the DAP port + call vimspector#LaunchWithSettings( { 'DAPPort': s:jdt_ls_debugger_port } ) + endfunction + + nnoremap :call StartDebugging() +< +You can then use '' to start debugging rather than just ''. + +If you see "Unable to get DAP port - is JDT.LS initialized?", try running +':YcmCompleter ExecuteCommand vscode.java.startDebugSession' and note the +output. If you see an error like 'ResponseFailedException: Request failed: +-32601: No delegateCommandHandler for vscode.java.startDebugSession', make sure +that: _Your YCM jdt.ls is actually working, see the YCM docs [44] for +troubleshooting_ The YCM jdt.ls has had time to initialize before you start the +debugger * That 'g:ycm_java_jdtls_extension_path' is set in '.vimrc' or prior +to YCM starting + +For the launch arguments, see the vscode document [45]. + +------------------------------------------------------------------------------- + *vimspector-other-lsp-clients* +Other LSP clients ~ + +See this issue [46] for more background. + +------------------------------------------------------------------------------- + *vimspector-lua* +Lua ~ + +Lua is supported through local-lua-debugger-vscode [47]. This debugger uses +stdio to communicate with the running process, so calls to 'io.read' will cause +problems. + +- './install_gadget.py --enable-lua' or ':VimspectorInstall + local-lua-debugger-vscode' +- Examples: 'support/test/lua/simple' and 'support/test/lua/love' +> + { + "$schema": "https://puremourning.github.io/vimspector/schema/vimspector.schema.json#", + "configurations": { + "lua": { + "adapter": "lua-local", + "configuration": { + "request": "launch", + "type": "lua-local", + "cwd": "${workspaceFolder}", + "program": { + "lua": "lua", + "file": "${file}" + } + } + }, + "luajit": { + "adapter": "lua-local", + "configuration": { + "request": "launch", + "type": "lua-local", + "cwd": "${workspaceFolder}", + "program": { + "lua": "luajit", + "file": "${file}" + } + } + }, + "love": { + "adapter": "lua-local", + "configuration": { + "request": "launch", + "type": "lua-local", + "cwd": "${workspaceFolder}", + "program": { + "command": "love" + }, + "args": ["${workspaceFolder}"] + } + } + } + } +< +------------------------------------------------------------------------------- + *vimspector-other-servers* +Other servers ~ + +- Java - vscode-javac. This works, but is not as functional as Java Debug + Server. Take a look at this comment [48] for instructions. + +=============================================================================== + *vimspector-customisation* +Customisation ~ + +There is very limited support for customisation of the UI. + +------------------------------------------------------------------------------- + *vimspector-changing-default-signs* +Changing the default signs ~ + +Vimsector uses the following signs internally. If they are defined before +Vimsector uses them, they will not be replaced. So to customise the signs, +define them in your 'vimrc'. + +============================================================================= +| _Sign_ | _Description_ | _Priority_ | +============================================================================= +| 'vimspectorBP' | Line breakpoint | 9 | +----------------------------------------------------------------------------- +| 'vimspectorBPCond' | Conditional line breakpoint | 9 | +----------------------------------------------------------------------------- +| 'vimspectorBPDisabled' | Disabled breakpoint | 9 | +----------------------------------------------------------------------------- +| 'vimspectorPC' | Program counter (i.e. current line) | 200 | +----------------------------------------------------------------------------- +| 'vimspectorPCBP' | Program counter and breakpoint | 200 | +----------------------------------------------------------------------------- + + +The default symbols are the equivalent of something like the following: +> + sign define vimspectorBP text=\ ● texthl=WarningMsg + sign define vimspectorBPCond text=\ ◆ texthl=WarningMsg + sign define vimspectorBPDisabled text=\ ● texthl=LineNr + sign define vimspectorPC text=\ ▶ texthl=MatchParen linehl=CursorLine + sign define vimspectorPCBP text=●▶ texthl=MatchParen linehl=CursorLine +< +If the signs don't display properly, your font probably doesn't contain these +glyphs. You can easily change them by defining the sign in your vimrc. For +example, you could put this in your 'vimrc' to use some simple ASCII symbols: +> + sign define vimspectorBP text=o texthl=WarningMsg + sign define vimspectorBPCond text=o? texthl=WarningMsg + sign define vimspectorBPDisabled text=o! texthl=LineNr + sign define vimspectorPC text=\ > texthl=MatchParen + sign define vimspectorPCBP text=o> texthl=MatchParen +< +------------------------------------------------------------------------------- + *vimspector-sign-priority* +Sign priority ~ + +Many different plugins provide signs for various purposes. Examples include +diagnostic signs for code errors, etc. Vim provides only a single priority to +determine which sign should be displayed when multiple signs are placed at a +single line. If you are finding that other signs are interfering with +vimspector's (or vice-versa), you can customise the priority used by vimspector +by setting the following dictionary: +> + let g:vimspector_sign_priority = { + \ '': , + \ } +< +For example: +> + let g:vimspector_sign_priority = { + \ 'vimspectorBP': 3, + \ 'vimspectorBPCond': 2, + \ 'vimspectorBPDisabled': 1, + \ 'vimspectorPC': 999, + \ } +< +All keys are optional. If a sign is not customised, the default priority it +used (as shown above). + +See ':help sign-priority'. The default priority is 10, larger numbers override +smaller ones. + +------------------------------------------------------------------------------- + *vimspector-changing-default-window-sizes* +Changing the default window sizes ~ + +**_Please Note_**: This customisation API is **_unstable_**, meaning that it +may change at any time. I will endeavour to reduce the impact of this and +announce changes in Gitter. + +The following options control the default sizes of the UI windows (all of them +are numbers) + +- 'g:vimspector_sidebar_width' (default: 50 columns): The width in columns of + the left utility windows (variables, watches, stack trace) + +- 'g:vimspector_bottombar_height' (default 10 lines): The height in rows of + the output window below the code window. + +Example: +> + let g:vimspector_sidebar_width = 75 + let g:vimspector_bottombar_height = 15 +< +------------------------------------------------------------------------------- + *vimspector-changing-terminal-size* +Changing the terminal size ~ + +The terminal is typically created as a vertical split to the right of the code +window, and that window is re-used for subsequent terminal buffers. The +following control the sizing of the terminal window used for debuggee +input/output when using Vim's built-in terminal. + +- 'g:vimspector_code_minwidth' (default: 82 columns): Minimum number of + columns to try and maintain for the code window when splitting to create + the terminal window. + +- 'g:vimspector_terminal_maxwidth' (default: 80 columns): Maximum number of + columns to use for the terminal. + +- 'g:vimspector_terminal_minwidth' (default: 10 columns): Minimum number of + columns to use when it is not possible to fit + 'g:vimspector_terminal_maxwidth' columns for the terminal. + +That's a lot of options, but essentially we try to make sure that there are at +least 'g:vimspector_code_minwidth' columns for the main code window and that +the terminal is no wider than 'g:vimspector_terminal_maxwidth' columns. +'g:vimspector_terminal_minwidth' is there to ensure that there's a reasonable +number of columns for the terminal even when there isn't enough horizontal +space to satisfy the other constraints. + +Example: +> + let g:vimspector_code_minwidth = 90 + let g:vimspector_terminal_maxwidth = 75 + let g:vimspector_terminal_minwidth = 20 +< +------------------------------------------------------------------------------- + *vimspector-custom-mappings-while-debugging* +Custom mappings while debugging ~ + +It's useful to be able to define mappings only while debugging and remove those +mappings when debugging is complete. For this purpose, Vimspector provides 2 +'User' autocommands: + +- 'VimspectorJumpedToFrame' - triggered whenever a 'break' event happens, or + when selecting a stack from to jump to. This can be used to create (for + example) buffer-local mappings for any files opened in the code window. + +- 'VimspectorDebugEnded' - triggered when the debug session is terminated + (actually when Vimspector is fully reset) + +An example way to use this is included in 'support/custom_ui_vimrc'. In there, +these autocommands are used to create buffer-local mappings for any files +visited while debugging and to clear them when completing debugging. This is +particularly useful for commands like 'VimspectorBalloonEval' which only +make sense while debugging (and only in the code window). Check the commented +section 'Custom mappings while debugging'. + +NOTE: This is a fairly advanced feature requiring some nontrivial vimscript. +It's possible that this feature will be incorporated into Vimspector in future +as it is a common requirement. + +------------------------------------------------------------------------------- + *vimspector-advanced-ui-customisation* +Advanced UI customisation ~ + +**_Please Note_**: This customisation API is **_unstable_**, meaning that it +may change at any time. I will endeavour to reduce the impact of this and +announce changes in Gitter. + +The above customisation of window sizes is limited intentionally to keep things +simple. Vimspector also provides a way for you to customise the UI without +restrictions, by running a 'User' autocommand just after creating the UI or +opening the terminal. This requires you to write some vimscript, but allows you +to do things like: + +- Hide a particular window or windows +- Move a particular window or windows +- Resize windows +- Have multiple windows for a particular buffer (say, you want 2 watch + windows) +- etc. + +You can essentially do anything you could do manually by writing a little +vimscript code. + +The 'User' autocommand is raised with 'pattern' set with the following values: + +- 'VimspectorUICreated': Just after setting up the UI for a debug session +- 'VimspectorTerminalOpened': Just after opening the terminal window for + program input/output. + +The following global variable is set up for you to get access to the UI +elements: 'g:vimspector_session_windows'. This is a 'dict' with the following +keys: + +- 'g:vimspector_session_windows.tagpage': The tab page for the session +- 'g:vimspector_session_windows.variables': Window ID of the variables + window, containing the 'vimspector.Variables' buffer. +- 'g:vimspector_session_windows.watches': Window ID of the watches window, + containing the 'vimspector.Watches' buffer. +- 'g:vimspector_session_windows.stack_trace': Window ID of the stack trade + window containing the 'vimspector.StackTrace' buffer. +- 'g:vimspector_session_windows.code': Window ID of the code window. +- 'g:vimspector_session_windows.output': Window ID of the output window. + +In addition, the following key is added when triggering the +'VimspectorTerminalOpened' event: + +- 'g:vimspector_session_windows.terminal': Window ID of the terminal window + +------------------------------------------------------------------------------- + *vimspector-customising-winbar* +Customising the WinBar ~ + +You can even customise the WinBar buttons by simply running the usual 'menu' +(and 'unmenu') commands. + +By default, Vimspector uses something a bit like this: +> + nnoremenu WinBar.■\ Stop :call vimspector#Stop( { 'interactive': v:false } ) + nnoremenu WinBar.▶\ Cont :call vimspector#Continue() + nnoremenu WinBar.▷\ Pause :call vimspector#Pause() + nnoremenu WinBar.↷\ Next :call vimspector#StepOver() + nnoremenu WinBar.→\ Step :call vimspector#StepInto() + nnoremenu WinBar.←\ Out :call vimspector#StepOut() + nnoremenu WinBar.⟲: :call vimspector#Restart() + nnoremenu WinBar.✕ :call vimspector#Reset( { 'interactive': v:false } ) +< +If you prefer a different layout or if the unicode symbols don't render +correctly in your font, you can customise this in the 'VimspectorUICreated' +autocommand, for example: +> + func! CustomiseUI() + call win_gotoid( g:vimspector_session_windows.code ) + " Clear the existing WinBar created by Vimspector + nunmenu WinBar + " Cretae our own WinBar + nnoremenu WinBar.Kill :call vimspector#Stop( { 'interactive': v:true } ) + nnoremenu WinBar.Continue :call vimspector#Continue() + nnoremenu WinBar.Pause :call vimspector#Pause() + nnoremenu WinBar.Step\ Over :call vimspector#StepOver() + nnoremenu WinBar.Step\ In :call vimspector#StepInto() + nnoremenu WinBar.Step\ Out :call vimspector#StepOut() + nnoremenu WinBar.Restart :call vimspector#Restart() + nnoremenu WinBar.Exit :call vimspector#Reset() + endfunction + + augroup MyVimspectorUICustomistaion + autocmd! + autocmd User VimspectorUICreated call s:CustomiseUI() + augroup END +< +------------------------------------------------------------------------------- + *vimspector-example* +Example ~ + +There is some example code in 'support/custom_ui_vimrc' showing how you can use +the window IDs to modify various aspects of the UI using some basic vim +commands, primarily 'win_gotoid' function and the 'wincmd' ex command. + +To try this out 'vim -Nu support/custom_ui_vimrc '. + +Here's a rather smaller example. A simple way to use this is to drop it into a +file named 'my_vimspector_ui.vim' in '~/.vim/plugin' (or paste into your +'vimrc'): +> + " Set the basic sizes + let g:vimspector_sidebar_width = 80 + let g:vimspector_code_minwidth = 85 + let g:vimspector_terminal_minwidth = 75 + + function! s:CustomiseUI() + " Customise the basic UI... + + " Close the output window + call win_gotoid( g:vimspector_session_windows.output ) + q + endfunction + + function s:SetUpTerminal() + " Customise the terminal window size/position + " For some reasons terminal buffers in Neovim have line numbers + call win_gotoid( g:vimspector_session_windows.terminal ) + set norelativenumber nonumber + endfunction + + augroup MyVimspectorUICustomistaion + autocmd! + autocmd User VimspectorUICreated call s:CustomiseUI() + autocmd User VimspectorTerminalOpened call s:SetUpTerminal() + augroup END +< +=============================================================================== + *vimspector-faq* +FAQ ~ + +1. Q: Does it work? A: Yeah. It's a bit unpolished. + +2. Q: Does it work with _this_ language? A: Probably, but it won't + necessarily be easy to work out what to put in the '.vimspector.json'. As + you can see above, some of the servers aren't really editor agnostic, and + require very-specific unique handling. + +3. How do I stop it starting a new Terminal.app on macOS? See this comment + [49] + +4. Can I specify answers to the annoying questions about exception + breakpoints in my '.vimspector.json' ? Yes, see here [23]. + +5. Do I have to specify the file to execute in '.vimspector.json', or could + it be the current vim file? You don't need to. You can specify $file for + the current active file. See here [20] for complete list of replacements + in the configuration file. + +6. You allow comments in '.vimspector.json', but Vim highlights these as + errors, do you know how to make this not-an-error? Yes, put this in + '~/.vim/after/syntax/json.vim': +> + syn region jsonComment start="/\*" end="\*/" + hi link jsonCommentError Comment + hi link jsonComment Comment +< +1. What is the difference between a 'gadget' and an 'adapter'? A gadget is + something you install with ':VimspectorInstall' or 'install_gadget.py', + an 'adapter' is something that Vimspector talks to (actually it's the + Vimspector config describing that thing). These are _usually_ one-to-one, + but in theory a single gadget can supply multiple 'adapter' configs. + Typically this happens when a 'gadget' supplies different 'adapter' + config for, say remote debugging, or debugging in a container, etc. + +2. The signs and winbar display funny symbols. How do I fix them? See this + and this + +3. What's this telemetry stuff all about? Are you sending my data to evil + companies? Debug adapters (for some reason) send telemetry data to + clients. Vimspector simply displays this information in the output + window. It _does not_ and _will not ever_ collect, use, forward or + otherwise share any data with any third parties. + +=============================================================================== + *vimspector-references* +References ~ + +[1] https://puremourning.github.io/vimspector-web/ +[2] https://puremourning.github.io/vimspector/configuration.html +[3] https://github.com/puremourning/vimspector/workflows/Build/badge.svg?branch=master +[4] https://gitter.im/vimspector/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge +[5] https://badges.gitter.im/vimspector/Lobby.svg +[6] https://puremourning.github.io/vimspector-web/img/vimspector-overview.png +[7] https://asciinema.org/a/VmptWmFHTNLPfK3DVsrR2bv8S +[8] https://asciinema.org/a/VmptWmFHTNLPfK3DVsrR2bv8S.svg +[9] https://asciinema.org/a/1wZJSoCgs3AvjkhKwetJOJhDh +[10] https://asciinema.org/a/1wZJSoCgs3AvjkhKwetJOJhDh.svg +[11] https://github.com/go-delve/delve +[12] https://github.com/puremourning/vimspector/wiki/languages +[13] https://asciinema.org/a/Hfu4ZvuyTZun8THNen9FQbTay +[14] https://asciinema.org/a/Hfu4ZvuyTZun8THNen9FQbTay.svg +[15] https://trello.com/b/yvAKK0rD/vimspector +[16] http://www.apache.org/licenses/LICENSE-2.0 +[17] https://greyhoundrescuewales.co.uk +[18] https://www.cancerresearchuk.org +[19] https://iccf.nl +[20] https://puremourning.github.io/vimspector/configuration.html#replacements-and-variables +[21] https://puremourning.github.io/vimspector/configuration.html#configuration-selection +[22] €mappings +[23] https://puremourning.github.io/vimspector/configuration.html#exception-breakpoints +[24] https://puremourning.github.io/vimspector-web/img/vimspector-code-window.png +[25] https://puremourning.github.io/vimspector-web/img/vimspector-locals-window.png +[26] https://puremourning.github.io/vimspector-web/img/vimspector-variable-eval-hover.png +[27] https://puremourning.github.io/vimspector-web/img/vimspector-watch-window.png +[28] https://puremourning.github.io/vimspector-web/img/vimspector-callstack-window.png +[29] https://puremourning.github.io/vimspector-web/img/vimspector-output-window.png +[30] https://github.com/Microsoft/vscode-cpptools +[31] https://puremourning.github.io/vimspector/configuration.html#remote-debugging-support +[32] https://github.com/vadimcn/vscode-lldb#features +[33] https://github.com/microsoft/debugpy +[34] https://github.com/microsoft/debugpy#debugpy-cli-usage +[35] https://github.com/microsoft/debugpy/wiki/Debug-configuration-settings +[36] https://github.com/microsoft/debugpy/wiki/Debugging-over-SSH +[37] https://github.com/Microsoft/vscode-python +[38] https://github.com/puremourning/vimspector/issues/105 +[39] https://github.com/puremourning/TclProDebug +[40] https://github.com/go-delve/delve/tree/master/Documentation/installation +[41] https://github.com/golang/vscode-go/blob/master/docs/debugging.md#troubleshooting +[42] https://github.com/Microsoft/java-debug +[43] https://github.com/ycm-core/YouCompleteMe#java-semantic-completion +[44] https://github.com/ycm-core/YouCompleteMe#troubleshooting +[45] https://code.visualstudio.com/docs/java/java-debugging +[46] https://github.com/puremourning/vimspector/issues/3 +[47] https://github.com/tomblind/local-lua-debugger-vscode +[48] https://github.com/puremourning/vimspector/issues/3#issuecomment-576916076 +[49] https://github.com/puremourning/vimspector/issues/90#issuecomment-577857322 + +vim: ft=help diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 0000000..1cabae3 --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,5 @@ +_site +.sass-cache +.jekyll-metadata +vendor/ +.bundle/ diff --git a/docs/404.html b/docs/404.html new file mode 100644 index 0000000..c472b4e --- /dev/null +++ b/docs/404.html @@ -0,0 +1,24 @@ +--- +layout: default +--- + + + +
+

404

+ +

Page not found :(

+

The requested page could not be found.

+
diff --git a/docs/Gemfile b/docs/Gemfile new file mode 100644 index 0000000..18f79a6 --- /dev/null +++ b/docs/Gemfile @@ -0,0 +1,32 @@ +source "https://rubygems.org" + +# Hello! This is where you manage which Jekyll version is used to run. +# When you want to use a different version, change it below, save the +# file and run `bundle install`. Run Jekyll with `bundle exec`, like so: +# +# bundle exec jekyll serve +# +# This will help ensure the proper Jekyll version is running. +# Happy Jekylling! +#gem "jekyll", "~> 3.8.5" + +# This is the default theme for new Jekyll sites. You may change this to anything you like. +gem "minima", "~> 2.0" + +# If you want to use GitHub Pages, remove the "gem "jekyll"" above and +# uncomment the line below. To upgrade, run `bundle update github-pages`. +gem "github-pages", group: :jekyll_plugins + +# If you have any plugins, put them here! +group :jekyll_plugins do + gem "jekyll-feed", "~> 0.6" +end + +# Windows does not include zoneinfo files, so bundle the tzinfo-data gem +gem "tzinfo-data", platforms: [:mingw, :mswin, :x64_mingw, :jruby] + +# Performance-booster for watching directories on Windows +gem "wdm", "~> 0.1.0" if Gem.win_platform? + + +gem "webrick", "~> 1.7" diff --git a/docs/Gemfile.lock b/docs/Gemfile.lock new file mode 100644 index 0000000..acf20f2 --- /dev/null +++ b/docs/Gemfile.lock @@ -0,0 +1,270 @@ +GEM + remote: https://rubygems.org/ + specs: + activesupport (6.0.3.6) + concurrent-ruby (~> 1.0, >= 1.0.2) + i18n (>= 0.7, < 2) + minitest (~> 5.1) + tzinfo (~> 1.1) + zeitwerk (~> 2.2, >= 2.2.2) + addressable (2.8.0) + public_suffix (>= 2.0.2, < 5.0) + coffee-script (2.4.1) + coffee-script-source + execjs + coffee-script-source (1.11.1) + colorator (1.1.0) + commonmarker (0.17.13) + ruby-enum (~> 0.5) + concurrent-ruby (1.1.8) + dnsruby (1.61.5) + simpleidn (~> 0.1) + em-websocket (0.5.2) + eventmachine (>= 0.12.9) + http_parser.rb (~> 0.6.0) + ethon (0.12.0) + ffi (>= 1.3.0) + eventmachine (1.2.7) + execjs (2.7.0) + faraday (1.3.0) + faraday-net_http (~> 1.0) + multipart-post (>= 1.2, < 3) + ruby2_keywords + faraday-net_http (1.0.1) + ffi (1.15.0) + forwardable-extended (2.6.0) + gemoji (3.0.1) + github-pages (214) + github-pages-health-check (= 1.17.0) + jekyll (= 3.9.0) + jekyll-avatar (= 0.7.0) + jekyll-coffeescript (= 1.1.1) + jekyll-commonmark-ghpages (= 0.1.6) + jekyll-default-layout (= 0.1.4) + jekyll-feed (= 0.15.1) + jekyll-gist (= 1.5.0) + jekyll-github-metadata (= 2.13.0) + jekyll-mentions (= 1.6.0) + jekyll-optional-front-matter (= 0.3.2) + jekyll-paginate (= 1.1.0) + jekyll-readme-index (= 0.3.0) + jekyll-redirect-from (= 0.16.0) + jekyll-relative-links (= 0.6.1) + jekyll-remote-theme (= 0.4.3) + jekyll-sass-converter (= 1.5.2) + jekyll-seo-tag (= 2.7.1) + jekyll-sitemap (= 1.4.0) + jekyll-swiss (= 1.0.0) + jekyll-theme-architect (= 0.1.1) + jekyll-theme-cayman (= 0.1.1) + jekyll-theme-dinky (= 0.1.1) + jekyll-theme-hacker (= 0.1.2) + jekyll-theme-leap-day (= 0.1.1) + jekyll-theme-merlot (= 0.1.1) + jekyll-theme-midnight (= 0.1.1) + jekyll-theme-minimal (= 0.1.1) + jekyll-theme-modernist (= 0.1.1) + jekyll-theme-primer (= 0.5.4) + jekyll-theme-slate (= 0.1.1) + jekyll-theme-tactile (= 0.1.1) + jekyll-theme-time-machine (= 0.1.1) + jekyll-titles-from-headings (= 0.5.3) + jemoji (= 0.12.0) + kramdown (= 2.3.1) + kramdown-parser-gfm (= 1.1.0) + liquid (= 4.0.3) + mercenary (~> 0.3) + minima (= 2.5.1) + nokogiri (>= 1.10.4, < 2.0) + rouge (= 3.26.0) + terminal-table (~> 1.4) + github-pages-health-check (1.17.0) + addressable (~> 2.3) + dnsruby (~> 1.60) + octokit (~> 4.0) + public_suffix (>= 2.0.2, < 5.0) + typhoeus (~> 1.3) + html-pipeline (2.14.0) + activesupport (>= 2) + nokogiri (>= 1.4) + http_parser.rb (0.6.0) + i18n (0.9.5) + concurrent-ruby (~> 1.0) + jekyll (3.9.0) + addressable (~> 2.4) + colorator (~> 1.0) + em-websocket (~> 0.5) + i18n (~> 0.7) + jekyll-sass-converter (~> 1.0) + jekyll-watch (~> 2.0) + kramdown (>= 1.17, < 3) + liquid (~> 4.0) + mercenary (~> 0.3.3) + pathutil (~> 0.9) + rouge (>= 1.7, < 4) + safe_yaml (~> 1.0) + jekyll-avatar (0.7.0) + jekyll (>= 3.0, < 5.0) + jekyll-coffeescript (1.1.1) + coffee-script (~> 2.2) + coffee-script-source (~> 1.11.1) + jekyll-commonmark (1.3.1) + commonmarker (~> 0.14) + jekyll (>= 3.7, < 5.0) + jekyll-commonmark-ghpages (0.1.6) + commonmarker (~> 0.17.6) + jekyll-commonmark (~> 1.2) + rouge (>= 2.0, < 4.0) + jekyll-default-layout (0.1.4) + jekyll (~> 3.0) + jekyll-feed (0.15.1) + jekyll (>= 3.7, < 5.0) + jekyll-gist (1.5.0) + octokit (~> 4.2) + jekyll-github-metadata (2.13.0) + jekyll (>= 3.4, < 5.0) + octokit (~> 4.0, != 4.4.0) + jekyll-mentions (1.6.0) + html-pipeline (~> 2.3) + jekyll (>= 3.7, < 5.0) + jekyll-optional-front-matter (0.3.2) + jekyll (>= 3.0, < 5.0) + jekyll-paginate (1.1.0) + jekyll-readme-index (0.3.0) + jekyll (>= 3.0, < 5.0) + jekyll-redirect-from (0.16.0) + jekyll (>= 3.3, < 5.0) + jekyll-relative-links (0.6.1) + jekyll (>= 3.3, < 5.0) + jekyll-remote-theme (0.4.3) + addressable (~> 2.0) + jekyll (>= 3.5, < 5.0) + jekyll-sass-converter (>= 1.0, <= 3.0.0, != 2.0.0) + rubyzip (>= 1.3.0, < 3.0) + jekyll-sass-converter (1.5.2) + sass (~> 3.4) + jekyll-seo-tag (2.7.1) + jekyll (>= 3.8, < 5.0) + jekyll-sitemap (1.4.0) + jekyll (>= 3.7, < 5.0) + jekyll-swiss (1.0.0) + jekyll-theme-architect (0.1.1) + jekyll (~> 3.5) + jekyll-seo-tag (~> 2.0) + jekyll-theme-cayman (0.1.1) + jekyll (~> 3.5) + jekyll-seo-tag (~> 2.0) + jekyll-theme-dinky (0.1.1) + jekyll (~> 3.5) + jekyll-seo-tag (~> 2.0) + jekyll-theme-hacker (0.1.2) + jekyll (> 3.5, < 5.0) + jekyll-seo-tag (~> 2.0) + jekyll-theme-leap-day (0.1.1) + jekyll (~> 3.5) + jekyll-seo-tag (~> 2.0) + jekyll-theme-merlot (0.1.1) + jekyll (~> 3.5) + jekyll-seo-tag (~> 2.0) + jekyll-theme-midnight (0.1.1) + jekyll (~> 3.5) + jekyll-seo-tag (~> 2.0) + jekyll-theme-minimal (0.1.1) + jekyll (~> 3.5) + jekyll-seo-tag (~> 2.0) + jekyll-theme-modernist (0.1.1) + jekyll (~> 3.5) + jekyll-seo-tag (~> 2.0) + jekyll-theme-primer (0.5.4) + jekyll (> 3.5, < 5.0) + jekyll-github-metadata (~> 2.9) + jekyll-seo-tag (~> 2.0) + jekyll-theme-slate (0.1.1) + jekyll (~> 3.5) + jekyll-seo-tag (~> 2.0) + jekyll-theme-tactile (0.1.1) + jekyll (~> 3.5) + jekyll-seo-tag (~> 2.0) + jekyll-theme-time-machine (0.1.1) + jekyll (~> 3.5) + jekyll-seo-tag (~> 2.0) + jekyll-titles-from-headings (0.5.3) + jekyll (>= 3.3, < 5.0) + jekyll-watch (2.2.1) + listen (~> 3.0) + jemoji (0.12.0) + gemoji (~> 3.0) + html-pipeline (~> 2.2) + jekyll (>= 3.0, < 5.0) + kramdown (2.3.1) + rexml + kramdown-parser-gfm (1.1.0) + kramdown (~> 2.0) + liquid (4.0.3) + listen (3.5.1) + rb-fsevent (~> 0.10, >= 0.10.3) + rb-inotify (~> 0.9, >= 0.9.10) + mercenary (0.3.6) + mini_portile2 (2.5.1) + minima (2.5.1) + jekyll (>= 3.5, < 5.0) + jekyll-feed (~> 0.9) + jekyll-seo-tag (~> 2.1) + minitest (5.14.4) + multipart-post (2.1.1) + nokogiri (1.11.5) + mini_portile2 (~> 2.5.0) + racc (~> 1.4) + octokit (4.20.0) + faraday (>= 0.9) + sawyer (~> 0.8.0, >= 0.5.3) + pathutil (0.16.2) + forwardable-extended (~> 2.6) + public_suffix (4.0.6) + racc (1.5.2) + rb-fsevent (0.10.4) + rb-inotify (0.10.1) + ffi (~> 1.0) + rexml (3.2.5) + rouge (3.26.0) + ruby-enum (0.9.0) + i18n + ruby2_keywords (0.0.4) + rubyzip (2.3.0) + safe_yaml (1.0.5) + sass (3.7.4) + sass-listen (~> 4.0.0) + sass-listen (4.0.0) + rb-fsevent (~> 0.9, >= 0.9.4) + rb-inotify (~> 0.9, >= 0.9.7) + sawyer (0.8.2) + addressable (>= 2.3.5) + faraday (> 0.8, < 2.0) + simpleidn (0.2.1) + unf (~> 0.1.4) + terminal-table (1.8.0) + unicode-display_width (~> 1.1, >= 1.1.1) + thread_safe (0.3.6) + typhoeus (1.4.0) + ethon (>= 0.9.0) + tzinfo (1.2.9) + thread_safe (~> 0.1) + unf (0.1.4) + unf_ext + unf_ext (0.0.7.7) + unicode-display_width (1.7.0) + webrick (1.7.0) + zeitwerk (2.4.2) + +PLATFORMS + ruby + +DEPENDENCIES + github-pages + jekyll-feed (~> 0.6) + minima (~> 2.0) + tzinfo-data + webrick (~> 1.7) + +BUNDLED WITH + 2.2.3 diff --git a/docs/README.local b/docs/README.local new file mode 100644 index 0000000..d891072 --- /dev/null +++ b/docs/README.local @@ -0,0 +1,13 @@ +To update/install: + +gem install bundler +bundle install --path vendor/bundle + +To run a local server/test the build + +bundle exec jekyll serve + +To update deps + +bundle update +or bundle update github-pages diff --git a/docs/_config.yml b/docs/_config.yml new file mode 100644 index 0000000..bc512c7 --- /dev/null +++ b/docs/_config.yml @@ -0,0 +1,42 @@ +# Welcome to Jekyll! +# +# This config file is meant for settings that affect your whole blog, values +# which you are expected to set up once and rarely edit after that. If you find +# yourself editing this file very often, consider using Jekyll's data files +# feature for the data you need to update frequently. +# +# For technical reasons, this file is *NOT* reloaded automatically when you use +# 'bundle exec jekyll serve'. If you change this file, please restart the server process. + +# Site settings +# These are used to personalize your new site. If you look in the HTML files, +# you will see them accessed via {{ site.title }}, {{ site.email }}, and so on. +# You can create any custom variable you would like, and they will be accessible +# in the templates via {{ site.myvariable }}. +title: Vimspector Documentation +description: | + Reference Documentation for Vimspector: A multi-language debugging front + end for Vim + +# Build settings +markdown: kramdown +theme: minima +plugins: + - jekyll-feed + +header_pages: + - index.md + - configuration.md + - schema/index.md + +# Exclude from processing. +# The following items will not be processed, by default. Create a custom list +# to override the default setting. +# exclude: +# - Gemfile +# - Gemfile.lock +# - node_modules +# - vendor/bundle/ +# - vendor/cache/ +# - vendor/gems/ +# - vendor/ruby/ diff --git a/docs/configuration.md b/docs/configuration.md new file mode 100644 index 0000000..3d524bf --- /dev/null +++ b/docs/configuration.md @@ -0,0 +1,1043 @@ +--- +title: Configuration +--- + + +This document defines the supported format for project and adapter configuration +for Vimspector. + + + * [Concepts](#concepts) + * [Debug adapter configuration](#debug-adapter-configuration) + * [Debug profile configuration](#debug-profile-configuration) + * [Replacements and variables](#replacements-and-variables) + * [The splat operator](#the-splat-operator) + * [Default values](#default-values) + * [Coercing Types](#coercing-types) + * [Configuration Format](#configuration-format) + * [Files and locations](#files-and-locations) + * [Adapter configurations](#adapter-configurations) + * [Debug configurations](#debug-configurations) + * [Configuration selection](#configuration-selection) + * [Specifying a default configuration](#specifying-a-default-configuration) + * [Preventing automatic selection](#preventing-automatic-selection) + * [Exception Breakpoints](#exception-breakpoints) + * [Predefined Variables](#predefined-variables) + * [Remote Debugging Support](#remote-debugging-support) + * [Python (debugpy) Example](#python-debugpy-example) + * [C-family (gdbserver) Example](#c-family-gdbserver-example) + * [Docker Example](#docker-example) + * [Appendix: Configuration file format](#appendix-configuration-file-format) + * [Appendix: Editor configuration](#appendix-editor-configuration) + + + + + +## Concepts + +As Vimspector supports debugging arbitrary projects, you need to tell it a few +details about what you want to debug, and how to go about doing that. + +In order to debug things, Vimspector requires a Debug Adapter which bridges +between Vimspector and the actual debugger tool. Vimspector can be used with any +debug adapter that implements the [Debug Adapter Protocol][dap]. + +For each debugging session, you provide a _debug configuration_ which includes +things like: + +- The debug adapter to use (and possibly how to launch and configure it). +- How to connect to the remote host, if remote debugging. +- How to launch or attach to your process. + +Along with optional additional configuration for things like: + +- Exception breakpoints + +### Debug adapter configuration + +The adapter to use for a particular debug session can be specified inline within +the _debug configuration_, but more usually the debug adapter is defined +separately and just referenced from the _debug configuration_. + +The adapter configuration includes things like: + +* How to launch or connect to the debug adapter +* How to configure it for PID attachment +* How to set up remote debugging, such as how to launch the process remotely + (for example, under `gdbserver`, `ptvsd`, etc.) + +### Debug profile configuration + +Projects can have many different debug profiles. For example you might have all +of the following, for a given source tree: + +* Remotely launch c++ the process, and break on `main` +* Locally Python test and break exception +* Remotely attach to a c++ process +* Locally launch a bash script +* Attach to a JVM listening on a port + +Each of these represents a different use case and a different _debug +configuration_. As mentioned above, a _debug configuration_ is essentially: + +* The adapter to use +* The type of session (launch or attach), and whether or not to do it remotely +* The configuration to pass to the adapter in order to launch or attach to the + process. + +The bulk of the configuration is the last of these, which comprises +adapter-specific options, as the Debug Adapter Protocol does not specify any +standard for launch or attach configuration. + +### Replacements and variables + +Vimspector _debug configuration_ is intended to be as general as possible, and +to be committed to source control so that debugging your applications becomes a +simple, quick and pain-free habit (e.g. answering questions like "what happens +if..." with "just hit F5 and step through!"). + +Therefore it's important to abstract certain details, like runtime and +build-time paths, and to parameterise the _debug configuration_. Vimspector +provides a simple mechanism to do this with `${replacement}` style replacements. + +The values available within the `${...}` are defined below, but in summary the +following are supported: + +* Environment variables, such as `${PATH}` +* Predefined variables, such as `${workspaceRoot}`, `${file}` etc. +* Configuration-defined variables, either provided by the adapter configuration + or debug configuration, or from running a simple shell command. +* Anything else you like - the user will be asked to provide a value. + +If the latter 2 are confusing, for now, suffice to say that they are how +Vimspector allows parameterisation of debug sessions. The [Vimspector +website][website-getting-started] has a good example of where this sort of thing +is useful: accepting the name of a test to run. + +But for now, consider the following example snippet: + +```json +{ + "configurations": { + "example-debug-configuration": { + // This is a single-line comment explaining the purpose + "adapter": "example-adapter-name", + "variables": { + "SecretToken": { // Variables should start with upper-case letters + "shell" : [ "cat", "${HOME}/.secret_token" ] + } + }, + "configuration": { + "request": "launch" /* or it could be "attach" */, + "program": [ + "${fileBasenameNoExtension}", + "-c", "configuration_file.cfg", + "-u", "${USER}", + "--test-identifier", "${TestIdentifier}", + "--secret-token", "${SecretToken}" + ] + }, + "breakpoints": { + "exception": { + "caught": "", + "uncaught": "Y" + } + } + } + } +} +``` + +In this (fictitious) example the `program` launch configuration item contains +the following variable substitutions: + +* `${fileBasenameNoExtension}` - this is a [Predefined + Variable](#predefined-variables), set by Vimspector to the base name of the + file that's opened in Vim, with its extension removed (`/path/to/xyz.cc` -> + `xyz`). +* `${USER}` - this refers to the Environment Variable `USER`. +* `${TestIdentifier}` - this variable is not defined, so the user is asked to + provide a value interactively when starting debugging. Vimspector remembers + what they said and provides it as the default should they debug again. +* `${SecretToken}` - this variable is provided by the configuration's + `variables` block. Its value is taken from the `strip`'d result of running + the shell command. Note these variables can be supplied by both the debug and + adapter configurations and can be either static strings or shell commands. + +#### The splat operator + +Often we want to create a single `.vimspector.json` entry which encompasses many +use cases, as it is tedious to write every use case/start up option in JSON. +This is why we have the replacement variables after all. + +Frequently debug adapters request command arguments as a JSON array, for +example: + +```json + "args": [ "one", "two three", "four" ], +``` + +To help with this sort of case, Vimspector supports a 'splat' operator for +replacement variables operating within lists. The syntax is: `"*${var}`, which +means roughly "splice the contents of `${var}` into the list at this position". +`${var}` is parsed like a shell command (using python's `shlex` parser) and each +word is added as a list item. + +For example: + +```json + "args": [ "*${CommandLineArgs}" ] +``` + +This would: + +* Ask the user to provide the variable `CommandLineArgs`. Let's say they entered + `one "two three" four` +* Split `CommandLineArgs` like shell arguments: `one`, `two three` and `four` +* Set `args` in the settings dict to: `[ "one", "two three", "four" ]` + +You can also combine with static values: + +```json + "args": [ "First", "*${CommandLineArgs}", "Last" ] +``` + +This would yield the intuitive result: +`[ "First", "one", "two three", "four", "Last" ]` + +#### Default values + +You can specify replacements with default values. In this case if the user has +not specified a value, they are prompted but with the default value +pre-populated, allowing them to just press return to accept the default. + +The syntax is `${variableName:default value}`. The default value can contain any +character, but to include a `}` you must escape it with a backslash. To include +a backslash in the JSON you must write `\\`, as in: + +```json + { "key": "${value:default {\\} stuff}" } +``` + +The default value can also be a replacement variable. However, this _must_ be a +variable that's already defined, such as one of the [predefined +variables](#predefined-variables), or one specified in a `variables` block. In +order to reference them, you _must_ use `${var}` syntax and you _must_ escape +the closing `}`. For example, the is a common and useful case: + +```json + { + "configuration": { + "program": "${script:${file\\}}" + } + } +``` + +This will prompt the user to specify `script`, but it will default to the path +to the current file. + +#### Coercing Types + +Sometimes, you want to provide an option for a boolean parameter, or want to +allow the user to specify more than just strings. Vimspector allows you to do +this, ensuring that the resulting JSON is valid. This is done by interpreting a +value as a JSON string and substituting the resulting JSON value in its place. + +This is easier to explain with an example. Let's say we want to offer the +ability to break on entry, as an option for the user. The launch configuration +requires `stopOnEntry` to be a bool. This doesn't work: + +```json + "stopOnEntry": "${StopOnEntry}" +``` + +The reason is that if the user types `true`, the resulting object is: + +```json + "stopOnEntry": "true" +``` + +The problem being that is a string, not a boolean. So Vimspector allows you to +re-interpret the string as a JSON value and use that instead. To do this, add +`#json` to the key's name. You can even add a default, like this: + +```json + "stopOnEntry#json": "${stopOnEntry:true}" +``` + +If the user accepts the default, the resulting string `"true"` is coerced to a +JSON value `true`, and the suffix is stripped fom the key, resulting in the +following: + +```json + "stopOnEntry": true +``` + +Which is what we need. + +If you happen to have a key that already ends in `#json` (unlikely!), then you +can force Vimspector to treat the value as a string by appending `#s`, as in: + +```json + "unlikelyKeyName#json#s": "this is a string, not JSON data" +``` + +***Advanced usage:*** + +The most common usage for this is for number and bool types, but it works for +objects too. If you want to be able to specify a whole object (e.g. a whole +`env` dict), then you can do that too: + +```json + "env#json": "${Environment:{\\}}" +``` + +The default value here is `{}` (note the `}` must be escaped!). The user can +then enter something like `{ "MYVAR": "MyValue", "OTHER": "Other" }` and the +resulting object would be: + +```json + "env": { + "MYVAR": "MyValue", + "OTHER": "Other" + } +``` + +It also works for lists, though [the splat operator](#the-splat-operator) +is usually more convenient for that. + +## Configuration Format + +All Vimspector configuration is defined in a JSON object. The complete +specification of this object is available in the [JSON Schema][schema], but +the basic format for the configuration object is: + +``` +{ + "adapters": { }, + "configurations": { } +} +``` + +The `adapters` key is actually optional, as `` can be +embedded within ``, though this is not recommended usage. + +## Files and locations + +The above configuration object is constructed from a number of configuration +files, by merging objects in a specified order. + +In a minimal sense, the only file required is a `.vimspector.json` file in the +root of your project which defines the [full configuration object][schema], but +it is usually useful to split the `adapters` configuration into a separate file +(or indeed one file per debug adapter). + +The following sections describe the files that are read and use the following +abbreviations: + +* `` means the path to the Vimspector installation (such as + `$HOME/.vim/pack/vimspector/start/vimspector`) +* `` is either `macos` or `linux` depending on the host operating system. +* `` is the Vim filetype. Where multiple filetypes are in effect, + typically all filetypes are checked. + +## Adapter configurations + +Vimspector reads a series of files to build the `adapters` object. The +`adapters` objects are merged in such a way that a definition for an adapter +named `example-adapter` in a later file _completely replaces_ a previous +definition. + +* `/gadgets//.gadgets.json` - the file written by + `install_gadget.py` and not usually edited by users. +* `/gadgets//.gadgets.d/*.json` (sorted alphabetically). + These files are user-supplied and override the above. +* The first such `.gadgets.json` file found in all parent directories of the + file open in Vim. +* The `.vimspector.json` and any filetype-specific configurations (see below) + +In all cases, the required format is: + +``` +{ + "$schema": "https://puremourning.github.io/vimspector/schema/gadgets.schema.json#", + "adapters": { + "": { + + } + } +} +``` + +Each adapters block can define any number of adapters. As mentioned, if the same +adapter name exists in multiple files, the last one read takes precedence and +_completely replaces_ the previous configuration. In particular that means you +can't just override one option, you have to override the whole block. + +Adapter configurations are re-read at the start of each debug session. + +The specification for the gadget object is defined in the [gadget schema][]. + +## Debug configurations + +There are two locations for debug configurations for a project: + +* `/configurations///*.json` +* `.vimspector.json` in the project source + +Typically, the debug configurations are read from `.vimspector.json`. The file +is found (like `.gadgets.json` above) by recursively searching up the directory +hierarchy from the directory of the file open in Vim. The first file found is +read and no further searching is done. + +Only a single `.vimspector.json` is read. If one is found, the location of this +file is used for `${workspaceRoot}` and other workspace-relative paths. + +In addition, users can create filetype-specific configurations in the Vimspector +installation directory. This can be useful where the parameters for the debug +session for a particular filetype are always known in advance, or can always be +entered by the user. This allows for debugging to "just work" without any +modification to the project source (no need to add a `.vimspector.json`). In +this case, the `${workspaceRoot}` and workspace-relative paths are interpreted +relative to the file open in Vim. This isn't ideal, but there is no other +obvious way to default this variable. + +As with gadgets, any debug configurations appearing within `.vimspector.json` +override any that appear in the common configuration dir. + +Debug configurations are re-read at the start of each debug session, so +modifications are picked up without any restarts of Vim. + +The specification for the gadget object is defined in the [schema][], but a +typical example looks like this: + +``` +{ + "$schema": "https://puremourning.github.io/vimspector/schema/vimspector.schema.json#", + "configurations": { + "": { + "adapter": "", + "configuration": { + "request": "", + + } + } + } +} +``` + +### Configuration selection + +When starting debugging, you can specify which debug configuration to launch +with `call vimspector#LaunchWithSettings( #{ configuration: 'name here' } )`. + +Otherwise, if there's only one configuration found, Vimspector will use that +configuration, unless it contains a key `"autoselect": false`. + +If multiple debug configurations are found, and no explicit configuration was +selected on Launch, the user is prompted to select a configuration, unless a +single debug configuration is found with a key `"default": true`. + +#### Specifying a default configuration + +As noted, you can specify a default configuration with `"default": true`: + +```json +{ + "configurations": { + "use this one": { + "default": true, + "adapter": " ... ", + "configuation": { + // ... + } + }, + "don't use this one": { + // ... + } + } +} +``` + +If multiple configurations are found with `default` set to `true`, then the +user is prompted anyway. + +#### Preventing automatic selection + +If you don't want a configuration to be selected automatically, then set +`"autoselect": false`. This particularly useful for configurations in the +central (as opposed to project-local) directory. For example: + +```json + "configurations": { + "Don't use this by default!": { + "autoselect": false, + "adapter": " ... ", + "configuation": { + // ... + } + } + } +``` + +Setting `autoselect` to `false` overrides setting `default` to `true`. + +### Exception Breakpoints + +Debug adapters have arbitrary configuration for exception breakpoints. Normally +this is presented as a series of question to the user on starting the debug +session. The question includes the name of the exception breakpoint option, +the default and the list of valid responses (usually `Y` or `N`). + +You can pre-configure the answers to these questions in the `breakpoints` +section of the debug configuration. For each question, take the name provided +and configure the response `exception` mapping in the `breakpoints` mapping. If +the configured response is empty string, the debug adapter default will be used. + +Referring to the above example, the following tells the debug adapter to use the +default value for `caught` exceptions and to break on `uncaught` exception: + +```json +{ + "configurations": { + "example-debug-configuration": { + "adapter": "example-adapter-name", + "breakpoints": { + "exception": { + "caught": "", + "uncaught": "Y" + } + }, + ... +``` + +The keys in the `exception` mapping are what Vimspector includes in the prompt. +For example, when prompted with the following: + +``` +cpp_throw: Break on C++: on throw (Y/N/default: Y)? +``` + +The exception breakpoint "type" is `cpp_throw` and the default is `Y`. + +Similarly: + +``` +cpp_catch: Break on C++: on catch (Y/N/default: N)? +``` + +The exception breakpoint "type" is `cpp_catch` and the default is `N`. + +Use the following to set the values in configuration and not get asked: + +```json + "configurations": { + "example-debug-configuration": { + "adapter": "example-adapter-name", + "breakpoints": { + "exception": { + "cpp_throw": "Y", + "cpp_catch": "Y" + } + }, +``` + +To just accept the defaults for these exception breakpoint types, don't specify +a value, as in : + +```json + "configurations": { + "example-debug-configuration": { + "adapter": "example-adapter-name", + "breakpoints": { + "exception": { + "cpp_throw": "", + "cpp_catch": "" + } + }, +``` + +## Predefined Variables + +The following variables are provided: + +* `${dollar}` - has the value `$`, can be used to enter a literal dollar +* `$$` - a literal dollar +* `${workspaceRoot}` - the path of the folder where `.vimspector.json` was + found +* `${workspaceFolder}` - the path of the folder where `.vimspector.json` was + found +* `${gadgetDir}` - path to the OS-specific gadget dir (`/gadgets/`) +* `${file}` - the current opened file +* `${relativeFile}` - the current opened file relative to `workspaceRoot` +* `${fileBasename}` - the current opened file's `basename` +* `${fileBasenameNoExtension}` - the current opened file's `basename` with no + file extension +* `${fileDirname}` - the current opened file's `dirname` +* `${fileExtname}` - the current opened file's extension +* `${cwd}` - the current working directory of the active window on launch +* `${unusedLocalPort}` - an unused local TCP port + +## Remote Debugging Support + +Vimspector has in-built support for executing remote debuggers (such as +`gdbserver`, `debugpy`, `llvm-server` etc.). This is useful for environments +where the development is done on one host and the runtime is +some other host, account, container, etc. + +In order for it to work, you have to set up paswordless SSH between the local +and remote machines/accounts. Then just tell Vimspector how to remotely launch +and/or attach to the app. + +This is presented as examples with commentary, as it's a fairly advanced/niche +case. If you're not already familiar with remote debugging tools (such as +gdbserver) or not familiar with ssh or such, you might need to independently +research that. + +Vimspector's tools are intended to automate your existing process for setting +this up rather than to offer batteries-included approach. Ultimately, all +Vimspector is going to do is run your commands over SSH, or docker, and +co-ordinate with the adapter. + +### Python (debugpy) Example + +Here is some examples using the Vimspector built-in +remote support (using SSH) to remotely launch and attach a python application +and connect to it using debugpy. + +The usage pattern is to hit ``, enter `host` (the host where your app runs), +`account` (the account it runs under), and `port` (a port that will be opened on +the remote host). Vimspector also supports exec'ing into Docker run containers +with `container` (the container name or id your app is running in). +Vimspector then orchestrates the various tools to set you up. + +```json + +{ + "adapters": { + "python-remote": { + "port": "${port}", + "host": "${host}", + "launch": { + "remote": { + "host": "${host}", // Remote host to ssh to (mandatory if not using container) + "account": "${account}", // User to connect as (optional) + + // Optional.... Manual additional arguments for ssh + // "ssh": { + // "args": [ "-o", "StrictHostKeyChecking=no" ] + // }, + + // Command to launch the debugee and attach the debugger; + // %CMD% replaced with the remote-cmdLine configured in the launch + // configuration. (mandatory) + "runCommand": [ + "python", "-m", "debugpy", + "--listen", "0.0.0.0:${port}", + "--wait-for-client", + "%CMD%" + ] + + // Optional alternative to runCommand (if you need to run multiple + // commands) + // "runCommands": [ + // [ /* first command */ ], + // [ /* second command */ ] + // ] + + } + + // optional delay to wait after running runCommand(s). This is often + // needed because of the way docker handles TCP, or if you're using some + // wrapper (e.g. to start the JVM) + // "delay": "1000m" // format as per :help sleep + }, + "attach": { + "remote": { + "host": "${host}", // Remote host to ssh to (mandatory if not using container) + "account": "${account}", // User to connect as (optional) + // Command to get the PID of the process to attach (mandatory) + "pidCommand": [ + // + // Remember taht you can use ${var} to ask for input. I use this to + // call a custom command to returm the PID for a named service, so + // here's an examle: + // + "/path/to/secret/script/GetPIDForService", "${ServiceName}" + ], + + // Command to attach the debugger; %PID% replaced with output of + // pidCommand above (mandatory) + "attachCommand": [ + "python", "-m", "debugpy", "--listen", "0.0.0.0:${port}", + "--pid", "%PID%" + ] + + // Optional alternative to attachCommand (if you need to run multiple + // commands) + // "attachCommands": [ + // [ /* first command */ ], + // [ /* second command */ ] + // ], + + // Optional.... useful with buggy gdbservers to kill -TRAP %PID% + // "initCompleteCommand": [ + // /* optional command to run after initialized */ + // ] + + // Optional.... Manual additional arguments for ssh + // "ssh": { + // "args": [ "-o", "StrictHostKeyChecking=no" ] + // }, + } + // optional delay to wait after running runCommand(s). This is often + // needed because of the way docker handles TCP, or if you're using some + // wrapper (e.g. to start the JVM) + // "delay": "1000m" // format as per :help sleep + } + } + }, + "configurations": { + "remote-launch": { + "adapter": "python-remote", + + "remote-request": "launch", + "remote-cmdLine": [ + "${RemoteRoot}/${fileBasename}", "*${args}" + ], + + "configuration": { + "request": "attach", + "pathMappings": [ + { + "localRoot": "${workspaceRoot}", + "remoteRoot": "${RemoteRoot}" + } + ] + } + }, + "remote-attach": { + "variables": { + // Just an example of how to specify a variable manually rather than + // vimspector asking for input from the user + "ServiceName": "${fileBasenameNoExtension}" + }, + + "adapter": "python-remote", + "remote-request": "attach", + + "configuration": { + "request": "attach", + "pathMappings": [ + { + "localRoot": "${workspaceRoot}", + "remoteRoot": "${RemoteRoot}" + } + ] + } + } + } +} +``` + +### C-family (gdbserver) Example + +This example uses Vimspector to remotely launch or attach to a binary using +`gdbserver` and then instructs vscode-cpptools to attach to that `gdbserver`. + +The approach is very similar to the above for python, just that we use gdbserver +and have to tell cpptools a few more options. + +```json +{ + "adapters": { + "cpptools-remote": { + "command": [ + "${gadgetDir}/vscode-cpptools/debugAdapters/OpenDebugAD7" + ], + "name": "cppdbg", + "configuration": { + "type": "cppdbg" + }, + "launch": { + "remote": { + "host": "${host}", + "account": "${account}", + "runCommand": [ + "gdbserver", + "--once", + "--no-startup-with-shell", + "--disable-randomisation", + "0.0.0.0:${port}", + "%CMD%" + } + }, + "attach": { + "remote": { + "host": "${host}", + "account": "${account}", + "pidCommand": [ + "/path/to/secret/script/GetPIDForService", "${ServiceName}" + ], + "attachCommand": [ + "gdbserver", + "--once", + "--attach", + "0.0.0.0:${port}", + "%PID%" + ], + // + // If your application is started by a wrapper script, then you might + // need the followin. GDB can't pause an application because it only + // sends the signal to the process group leader. Or something. + // Basically, if you find that everything just hangs and the + // application never attaches, try using the following to manually + // force the trap signal. + // + "initCompleteCommand": [ + "kill", + "-TRAP", + "%PID%" + ] + } + } + } + }, + "configurations": { + "remote launch": { + "adapter": "cpptools-remote", + "remote-cmdLine": [ "/path/to/the/remote/executable", "args..." ], + "remote-request": "launch", + "configuration": { + "request": "attach", // yes, attach! + + "program": "/path/to/the/local/executable", + "MIMode": "gdb", + "miDebuggerAddress": "${host}:${port}" + } + }, + "remote attach": { + "adapter": "cpptools-remote", + "remote-request": "attach", + "configuration": { + "request": "attach", + + "program": "/path/to/the/local/executable", + "MIMode": "gdb", + "miDebuggerAddress": "${host}:${port}" + } + } +} +``` + +### Docker Example + +This example uses Vimspector to remotely launch or attach to a docker container +port. + +``` json +{ + "adapters": { + "python-remote": { + "port": "${port}", + "launch": { + "remote": { + "container": "${container}", // Docker container id or name to exec into to. + + // Command to launch the debugee and attach the debugger; + // %CMD% replaced with the remote-cmdLine configured in the launch + // configuration. (mandatory) + "runCommand": [ + "python", "-m", "debugpy", + "--listen", "0.0.0.0:${port}", + "--wait-for-client", + "%CMD%" + ] + + // Optional alternative to runCommand (if you need to run multiple + // commands) + // "runCommands": [ + // [ /* first command */ ], + // [ /* second command */ ] + // ] + + } + + // optional delay to wait after running runCommand(s). This is often + // needed because of the way docker handles TCP + "delay": "1000m" // format as per :help sleep + }, + "attach": { + "remote": { + "container": "${container}", // Docker container id or name to exec into. + // Command to get the PID of the process to attach (mandatory) + // This command gets appended to "docker exec ${container}" + "pidCommand": [ + // + // Remember taht you can use ${var} to ask for input. I use this to + // call a custom command to returm the PID for a named service, so + // here's an examle: + // + "sh", "-c", "pgrep", "-f", "${filename}" + ], + + // Command to attach the debugger; %PID% replaced with output of + // pidCommand above (mandatory) + "attachCommand": [ + "sh", "-c", "python", "-m", "debugpy", "--listen", "0.0.0.0:${port}", + "--pid", "%PID%" + ] + + // Optional alternative to attachCommand (if you need to run multiple + // commands) + // "attachCommands": [ + // [ /* first command */ ], + // [ /* second command */ ] + // ], + + // Optional.... useful with buggy gdbservers to kill -TRAP %PID% + // "initCompleteCommand": [ + // /* optional command to run after initialized */ + // ] + + } + + // optional delay to wait after running runCommand(s). This is often + // needed because of the way docker handles TCP, or if you're using some + // wrapper (e.g. to start the JVM) + "delay": "1000m" // format as per :help sleep + } + } + }, + "configurations": { + "remote-launch": { + "adapter": "python-remote", + + "remote-request": "launch", + "remote-cmdLine": [ + "${RemoteRoot}/${fileBasename}", "*${args}" + ], + + "configuration": { + "request": "attach", + "pathMappings": [ + { + "localRoot": "${workspaceRoot}", + "remoteRoot": "${RemoteRoot}" + } + ] + } + }, + "remote-attach": { + "variables": { + // Just an example of how to specify a variable manually rather than + // vimspector asking for input from the user + "FileName": "${fileName}" + }, + + "adapter": "python-remote", + "remote-request": "attach", + + "configuration": { + "request": "attach", + "pathMappings": [ + { + "localRoot": "${workspaceRoot}", + "remoteRoot": "${RemoteRoot}" + } + ] + } + } + } +} +``` + +## Appendix: Configuration file format + +The configuration files are text files which must be UTF-8 encoded. They +contain a single JSON object, along with optional comments. + +Comments are "c-style", i.e.: + +* `// single line comment ...` +* `/* inline comment */` + +There is much debate about whether JSON files should contain comments. I have +added them because they are useful in the context of configuration files. +Unforutnately this may mean your editor doesn't like them (they are strictly +invalid JSON) so it's up to you if you use them. + +Technically, Vimspector uses [JSON +minify](https://github.com/getify/JSON.minify) to strip comments before parsing +the JSON. + +## Appendix: Editor configuration + +If you would like some assistance with writing the JSON files, and your editor +of choice has a way to use a language server, you can use the +[VSCode JSON language server][vscode-json]. + +It is recommended to include the `$schema` declaration as in the above examples, +but if that isn't present, the following [JSON language server +configuration][json-ls-config] is recommened to load the schema from the +Internet: + +```json +{ + "json": { + "schemas": [ + { + "fileMatch": [ ".vimspector.json" ], + "url": "https://puremourning.github.io/vimspector/schema/vimspector.schema.json" + }, + { + "fileMatch": [ ".gadgets.json", ".gadgets.d/*.json" ], + "url": "https://puremourning.github.io/vimspector/schema/gadgets.schema.json" + } + ] + } +} +``` + +If your language server client of choice happens to be [YouCompleteMe][], then +the following `.ycm_extra_conf.py` is good enough to get you going, after +following the instructions in the [lsp-examples][] repo to get the server set +up: + +```python +VIMSPECTOR_HOME = '/path/to/vimspector' # TODO: Change this + +def Settings( **kwargs ): + if kwargs[ 'language' ] == 'json': + return { + 'ls': { + 'json': { + 'schemas': [ + { + 'fileMatch': [ '.vimspector.json' ], + 'url': f'file://{VIMSPECTOR_HOME}/docs/schema/vimspector.schema.json' + }, + { + 'fileMatch': [ '.gadgets.json', '.gadgets.d/*.json' ], + 'url': f'file://{VIMSPECTOR_HOME}/docs/schema/gadgets.schema.json' + } + ] + } + } + } + + return None # Or your existing Settings definition.... +``` + +This configuration can be adapted to any other LSP-based editor configuration +and is provided just as an example. + +[dap]: https://microsoft.github.io/debug-adapter-protocol/ +[schema]: http://puremourning.github.io/vimspector/schema/vimspector.schema.json +[gadget-schema]: http://puremourning.github.io/vimspector/schema/gadgets.schema.json +[YouCompleteMe]: https://github.com/ycm-core/YouCompleteMe +[lsp-examples]: https://github.com/ycm-core/lsp-examples +[vscode-json]: https://github.com/vscode-langservers/vscode-json-languageserver +[json-ls-config]: https://github.com/vscode-langservers/vscode-json-languageserver#settings diff --git a/docs/custom_gadget_file.md b/docs/custom_gadget_file.md new file mode 100644 index 0000000..428b7d9 --- /dev/null +++ b/docs/custom_gadget_file.md @@ -0,0 +1,25 @@ +--- +title: Configuration +--- + +This document describes how to use vimspector's `install_gadget.py` to install +custom debug adapters. This can be useful as a way to get an adapter working +that isn't officially supported by Vimspector, but otherwise can be made to work +by simply downloading the VScode extension into the gadget directory. + +## Usage + +``` +./install_gadget.py --enable-custom=/path/to/a.json \ + --enable-custom=/path/to/b.json` +``` + +This tells `install_gadget.py` to read `a.json` and `b.json` as _gadget +definitions_ and download/unpack the specified gadgets into the gadget dir, just +like the supported adapters. + +## Gadget Definitions + +A _gadget definition_ is a file containing a single JSON object definition, +describing the debug adapter and how to download and install it. This mechanism +is crude but can be effective. diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..b568203 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,23 @@ +--- +title: Vimspector Reference Guide +--- + +This section contains reference material for configuring and using +[Vimspector][vimspector]. It is intentionally technical in nature and should +serve as a reference guide, rather than a user guide or tutorial. + +It complements the following: + +* The [Vimspector README][readme], which contains a general overview and is the + main go-to for getting started, installation, etc. +* The [Vimspector Website][website], which contains a step-by-step tutorial and + some detail about the user interface. + +## Contents + +* [Configuring Vimspector for your projects](configuration.html) +* [Full JSON Schema reference](schema/) + +[vimspector]: https://github.com/puremourning/vimspector +[readme]: https://github.com/puremourning/vimspector/blob/master/README.md +[website]: https://puremourning.github.io/vimspector-web diff --git a/docs/schema/gadgets.schema.json b/docs/schema/gadgets.schema.json new file mode 100644 index 0000000..b4aab3b --- /dev/null +++ b/docs/schema/gadgets.schema.json @@ -0,0 +1,11 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://puremourning.github.io/vimspector/schema/gadgets.schema.json", + "type": "object", + "properties": { + "adapters": { + "type": "object", + "additionalProperties": { "$ref": "vimspector.schema.json#/definitions/adapter" } + } + } +} diff --git a/docs/schema/index.md b/docs/schema/index.md new file mode 100644 index 0000000..2b1bb27 --- /dev/null +++ b/docs/schema/index.md @@ -0,0 +1,21 @@ +--- +title: Configuration Schema +--- + +This area contains the [JSON Schema][json-schema] representation of the +configuration objects used for configuring Vimspector. For more information on +JSON Schema, check out [Understanding JSON +Schema](http://json-schema.org/understanding-json-schema) + +Vimsepctor specification is based on [Draft 7][draft-7] of JSON Schema +standard. + +## The schemas + +* [`vimspector.schema.json`](vimspector.schema.json) - contains the full + configuration defnition (`.vimspector.json`). +* [`gadgets.schema.json`](gadgets.schema.json) - contains the specification for + gadget-only objects (`.gadgets.json`). + +[json-schema]: http://json-schema.org +[draft-7]: https://json-schema.org/specification-links.html#draft-7 diff --git a/docs/schema/vimspector.schema.json b/docs/schema/vimspector.schema.json new file mode 100644 index 0000000..cd79f11 --- /dev/null +++ b/docs/schema/vimspector.schema.json @@ -0,0 +1,298 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://puremourning.github.io/vimspector/schema/vimspector.schema.json", + "definitions": { + "variables": { + "description": "A mappoing of name/value pairs to set variables to be used elsewhere in the definition, or a list of such mappings.", + "properties": { + "variables": { + "oneOf": [ + { "$ref": "#/definitions/variables-mapping" }, + { "$ref": "#/definitions/variables-list" } + ] + } + } + }, + "variables-list": { + "type": "array", + "description": "A list of variable mappings. This can be useful where variable definitions rely on each other. By using a list, you can control the sequence in which variables are defined.", + "items": { "$ref": "#/definitions/variables-mapping" } + }, + "variables-mapping": { + "type": "object", + "additionalProperties": { + "oneOf": [ + { "type": "string" }, + { + "type": "object", + "required": [ "shell" ], + "properties": { + "shell": { + "type": [ "array", "string" ], + "description": "Command to run. If it's a string, it's split using Python's shelex splitting. Can contain other variable references." + }, + "cwd": { "type": "string" }, + "env": { "type": "object" } + } + } + ] + } + }, + "adapter-launchattach": { + "properties": { + "launch": { + "allOf": [ + { "$ref": "#/definitions/adapter-remote" }, + { + "properties": { + "delay": { + "type": "string", + "description": "A time in the format understood by :help :sleep to wait after running the attachCommand(s)" + } + } + } + ] + }, + "attach": { + "allOf": [ + { "$ref": "#/definitions/adapter-remote" }, + { + "type": "object", + "required": [ "pidSelect" ], + "properties": { + "pidSelect": { + "enum": [ "ask", "none" ] + }, + "pidProperty": { + "type": "string", + "description": "The launch config property which the PID should be injected into. Required when 'pidSelect' is 'ask'." + }, + "delay": { + "type": "string", + "description": "A time in the format understood by :help :sleep to wait after running the attachCommand(s)" + } + } + } + ] + } + } + }, + "adapter-remote": { + "type": "object", + "properties": { + "remote": { + "type": "object", + "description": "Configures how Vimspector will marshal remote debugging requests. When remote debugging, Vimspector will either ssh to 'account'@'host' or docker exec -it to 'container' and run 'pidCommand', 'attachCommands', 'runCommands', etc. based on the 'remote-command' option in the debug configuration. If 'remote-command' is 'launch', it runs 'runCommand(s)', otherwise (it's 'attach') vimspector runs 'pidCommand', followed by 'attachCommand(s)'.Then it starts up the debug adapter with the debug configuration as normal. Usually this is configured with an 'attach' request (whether we remotely 'launched' or not). Once the initialization exchange is complete, Vimspector runs the optional 'initCompleteCommand' which can be used to force the application to break, e.g. by sending it SIGINT. This is required on some platforms which have buggy gdbservers (for example)", + "allOf": [ + { + "oneOf": [ + { "required": [ "host" ] }, + { "required": [ "container" ] } + ] + }, + { + "properties": { + "account": { + "type": "string", + "description": "Remote account name used when ssh'ing. Defaults to the current user account." + }, + "host": { + "type": "string", + "description": "Name of the remote host to connect to (via passwordless SSH)." + }, + "container": { + "type": "string", + "description": "Name or container id of the docker run container to connect to (via docker exec). Note the container must already be running (Vimspector will not start it) and it must have the port forwarded to the host if subsequently connecting via a port (for example docker run -p 8765:8765 -it simple_python)." + } + } + }, + { + "oneOf": [ + { + "allOf": [ + { + "oneOf": [ + { "required": [ "attachCommand" ] }, + { "required": [ "attachCommands" ] } + ] + }, + { + "properties": { + "initCompleteCommand": { + "type": "array", + "items": { "type": "string" }, + "description": "For remote-attach. Remote command to execute after initialization of the debug adapter. Can be used to work around buggy attach behaviour on certain platforms (advanced usage). Can contain the special token %PID% which is replaced with the PID returned by 'pidCommand'" + }, + "pidCommand": { + "type": "array", + "items": { "type": "string" }, + "description": "Required for remote-attach. Remote command to execute to return the PID to attach to." + }, + "attachCommands": { + "type": [ "array" ], + "items": { "type": "array", "items": { "type": "string" } }, + "description": "For remote-attach. List of commands to execute remotely to set up the attach. Can contain the special token %PID% which is replaced with the PID returned by the remote 'pidCommand'." + }, + "attachCommand": { + "type": "array", + "items": { "type": "string" }, + "description": "A single command to execute for remote-attach. Like attachCommands but for a single command. If attachCommands is supplied, this is not used." + } + } + } + ] + }, + { + "allOf": [ + { + "oneOf": [ + { "required": [ "runCommand" ] }, + { "required": [ "runCommands" ] } + ] + }, + { + "properties": { + "runCommands": { + "type": [ "array" ], + "items": { "type": "array", "items": { "type": "string" } }, + "description": "For remote-launch. List of commands to execute remotely to set up the launch. An entry in the array can be the special token '%CMD%' which is replaced with the evaluated 'remote-cmdLine' value in the debug configuration. This is useful to parameterize launcging remotely under something like gdbserver." + }, + "runCommand": { + "type": "array", + "items": { "type": "string" }, + "description": "A single command to execute for remote-launch. Like runCommands but for a single command." + } + } + } + ] + } + ] + } + ] + } + } + }, + "adapter": { + "allOf": [ + { "type": "object" }, + { "$ref": "#/definitions/variables" }, + { + "properties": { + "name": { + "type": "string", + "description": "Passed to the adapter in the initialization request. Some adapters are particularly picky about what value goes here. Usually it can be omitted and Vimspector will send a generic value" + }, + "configuration": { + "type": "object", + "description": "Base debug configuration. Can be used to set default values for all debug configurations. When reading individual debug configurations from 'configurations', those configurations are merged with this object. Definitions in the debug configuration override anything in this object. Typical usage for this is to set the 'type' parameter, which some debug adapters are very picky about, or to set e.g. the path to an underlying debugger." + } + } + }, + { "$ref": "#/definitions/adapter-launchattach" }, + { + "anyOf": [ + { "required": [ "command" ] }, + { "required": [ "port" ] }, + { "required": [ "command", "port" ] } + ] + }, + { + "properties": { + "host": { + "type": "string", + "default": "127.0.0.1", + "description": "Connect to this host in multi-session mode" + }, + "port": { + "oneOf": [ + { "type": "string" }, + { "type": "integer" } + ], + "description": "If supplied, indicates that a socket connection should be made to this port on 'host'. If the value is 'ask', then the user is asked to enter the port number to connect to." + } + } + } + ] + } + }, + "type": "object", + "required": [ "configurations" ], + "properties": { + "adapters": { + "type": "object", + "additionalProperties": { "$ref": "#/definitions/adapter" } + }, + "configurations": { + "type": "object", + "additionalProperties": { + "allOf": [ + { "$ref": "#/definitions/variables" }, + { + "type": "object", + "properties": { + "adapter": { + "description": "Adapter configuration to use for this debug session", + "oneOf": [ + { "$ref": "#/definitions/adapter" }, + { + "type": "string", + "description": "Name of an adapter in the 'adapters' mapping" + } + ] + }, + "remote-request": { + "enum": [ "launch", "attach" ], + "description": "When the 'remote' block is defined in the adapter configuration, this can be used to override the actual action taken (remotely). Usually the actual 'configuration' will contain 'request' of 'attach', but in order to remotely 'launch' the process (e.g. under gdbserver or equivalent), use remote-attach set to 'launch'" + }, + "remote-cmdLine": { + "type": [ "string", "array" ], + "description": "Defines the value of the special token %CMD% in remote-launch 'runCommand(s)'. The value is inserted into the command line where an entry matching '%CMD%' is found in 'runCommand(s)' command array." + }, + "configuration": { + "type": "object", + "required": [ "request" ], + "properties": { + "request": { + "enum": [ "launch", "attach" ], + "description": "Type of session - launch process or attach to process" + } + }, + "additionalProperties": { + "description": "Additional properties are passed to the debug adapter in the 'launch' or 'attach' request and are specific to the debug adapter." + } + }, + "breakpoints": { + "type": "object", + "properties": { + "line": { + "$comment": "TBA: Not supported yet" + }, + "function": { + "$comment": "TBA: Not supported yet" + }, + "exception": { + "type": "object", + "description": "Exception breakpoints configuration, mapping the server's exception filter to enabled/disable/default flag", + "additionalProperties": { + "oneOf": [ + { + "type": "boolean", + "description": "true = enable, false = disable" + }, + { + "type": "string", + "enum": [ "Y", "N", "" ], + "description": "Y = enable, N = disable, '' = default" + } + ] + } + } + } + } + } + } + ] + } + } + } +} diff --git a/install_gadget.py b/install_gadget.py index 6da3b35..b6e2fd7 100755 --- a/install_gadget.py +++ b/install_gadget.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # vimspector - A multi-language debugging system for Vim # Copyright 2019 Ben Jackson @@ -15,506 +15,52 @@ # See the License for the specific language governing permissions and # limitations under the License. -try: - import urllib.request as urllib2 -except ImportError: - import urllib2 +import sys + +if sys.version_info.major < 3: + sys.exit( "You need to run this with python 3. Your version is " + + '.'.join( map( str, sys.version_info[ :3 ] ) ) ) import argparse -import contextlib import os -import string -import zipfile -import gzip -import shutil -import subprocess -import traceback -import tarfile -import hashlib -import sys import json import functools -import time - -try: - from io import BytesIO ## for Python 3 -except ImportError: - from BytesIO import BytesIO +import operator +import glob # Include vimspector source, for utils sys.path.insert( 1, os.path.join( os.path.dirname( __file__ ), 'python3' ) ) -from vimspector import install +from vimspector import install, installer, gadgets +from vimspector.vendor.json_minify import minify -GADGETS = { - 'vscode-cpptools': { - 'language': 'c', - 'download': { - 'url': 'https://github.com/Microsoft/vscode-cpptools/releases/download/' - '${version}/${file_name}', - }, - 'do': lambda name, root: InstallCppTools( name, root ), - 'all': { - 'version': '0.23.1', - }, - 'linux': { - 'file_name': 'cpptools-linux.vsix', - 'checksum': - 'c0f424bd6d5e016d70126587c80b92d981729c708ce524f2cce4c3f524b41d71' - }, - 'macos': { - 'file_name': 'cpptools-osx.vsix', - 'checksum': - '431692395ba243ea20428e083d5df3201a0dbda31a66eab7729da0f377def5fd', - }, - 'windows': { - 'file_name': 'cpptools-win32.vsix', - 'checksum': None, - }, - "adapters": { - "vscode-cpptools": { - "name": "cppdbg", - "command": [ - "${gadgetDir}/vscode-cpptools/debugAdapters/OpenDebugAD7" - ], - "attach": { - "pidProperty": "processId", - "pidSelect": "ask" - }, - }, - }, - }, - 'vscode-python': { - 'language': 'python', - 'download': { - 'url': 'https://github.com/Microsoft/vscode-python/releases/download/' - '${version}/${file_name}', - }, - 'all': { - 'version': '2019.10.41019', - 'file_name': 'ms-python-release.vsix', - 'checksum': - '38e8bf782fc6d2dc904868add2e1e5dc66197a06a902f6d17e15f96d4e9bf16b', - }, - 'adapters': { - "vscode-python": { - "name": "vscode-python", - "command": [ - "node", - "${gadgetDir}/vscode-python/out/client/debugger/debugAdapter/main.js", - ], - } - }, - }, - 'tclpro': { - 'language': 'tcl', - 'repo': { - 'url': 'https://github.com/puremourning/TclProDebug', - 'ref': 'f5c56b7067661ce84e205765060224076569ae0e', # master 26/10/2019 - }, - 'do': lambda name, root: InstallTclProDebug( name, root ) - }, - 'netcoredbg': { - 'language': 'csharp', - 'enabled': False, - 'download': { - 'url': 'https://github.com/Samsung/netcoredbg/releases/download/latest/' - '${file_name}', - 'format': 'tar', - }, - 'all': { - 'version': 'master' - }, - 'macos': { - 'file_name': 'netcoredbg-osx-master.tar.gz', - 'checksum': '', - }, - 'linux': { - 'file_name': 'netcoredbg-linux-master.tar.gz', - 'checksum': '', - }, - 'do': lambda name, root: MakeSymlink( gadget_dir, - name, - os.path.join( root, 'netcoredbg' ) ), - 'adapters': { - 'netcoredbg': { - "name": "netcoredbg", - "command": [ - "${gadgetDir}/netcoredbg/netcoredbg", - "--interpreter=vscode" - ], - "attach": { - "pidProperty": "processId", - "pidSelect": "ask" - }, - }, - } - }, - 'vscode-mono-debug': { - 'language': 'csharp', - 'enabled': False, - 'download': { - 'url': 'https://marketplace.visualstudio.com/_apis/public/gallery/' - 'publishers/ms-vscode/vsextensions/mono-debug/${version}/' - 'vspackage', - 'target': 'vscode-mono-debug.vsix.gz', - 'format': 'zip.gz', - }, - 'all': { - 'file_name': 'vscode-mono-debug.vsix', - 'version': '0.15.8', - 'checksum': - '723eb2b621b99d65a24f215cb64b45f5fe694105613a900a03c859a62a810470', - }, - 'adapters': { - 'vscode-mono-debug': { - "name": "mono-debug", - "command": [ - "mono", - "${gadgetDir}/vscode-mono-debug/bin/Release/mono-debug.exe" - ], - "attach": { - "pidSelect": "none" - }, - }, - } - }, - 'vscode-bash-debug': { - 'language': 'bash', - 'download': { - 'url': 'https://github.com/rogalmic/vscode-bash-debug/releases/' - 'download/${version}/${file_name}', - }, - 'all': { - 'file_name': 'bash-debug-0.3.5.vsix', - 'version': 'v0.3.5', - 'checksum': '', - } - }, - 'vscode-go': { - 'language': 'go', - 'download': { - 'url': 'https://github.com/microsoft/vscode-go/releases/download/' - '${version}/${file_name}' - }, - 'all': { - 'version': '0.11.4', - 'file_name': 'Go-0.11.4.vsix', - 'checksum': - 'ff7d7b944da5448974cb3a0086f4a2fd48e2086742d9c013d6964283d416027e' - }, - 'adapters': { - 'vscode-go': { - 'name': 'delve', - 'command': [ - 'node', - '${gadgetDir}/vscode-go/out/src/debugAdapter/goDebug.js' - ], - }, - }, - }, - 'vscode-node-debug2': { - 'language': 'node', - 'enabled': False, - 'repo': { - 'url': 'https://github.com/microsoft/vscode-node-debug2', - 'ref': 'v1.39.1', - }, - 'do': lambda name, root: InstallNodeDebug( name, root ), - 'adapters': { - 'vscode-node': { - 'name': 'node2', - 'type': 'node2', - 'command': [ - 'node', - '${gadgetDir}/vscode-node-debug2/out/src/nodeDebug.js' - ] - }, - }, - }, - 'debugger-for-chrome': { - 'language': 'chrome', - 'enabled': False, - 'download': { - 'url': 'https://marketplace.visualstudio.com/_apis/public/gallery/' - 'publishers/msjsdiag/vsextensions/' - 'debugger-for-chrome/${version}/vspackage', - 'target': 'msjsdiag.debugger-for-chrome-4.12.0.vsix.gz', - 'format': 'zip.gz', - }, - 'all': { - 'version': '4.12.0', - 'file_name': 'msjsdiag.debugger-for-chrome-4.12.0.vsix', - 'checksum': - '0df2fe96d059a002ebb0936b0003e6569e5a5c35260dc3791e1657d27d82ccf5' - }, - 'adapters': { - 'chrome': { - 'name': 'debugger-for-chrome', - 'type': 'chrome', - 'command': [ - 'node', - '${gadgetDir}/debugger-for-chrome/out/src/chromeDebug.js' - ], - }, - }, - }, -} +# ------------------------------------------------------------------------------ +# Entry point +# ------------------------------------------------------------------------------ -@contextlib.contextmanager -def CurrentWorkingDir( d ): - cur_d = os.getcwd() - try: - os.chdir( d ) - yield - finally: - os.chdir( cur_d ) +parser = argparse.ArgumentParser( + formatter_class = argparse.RawDescriptionHelpFormatter, + description = 'Install DAP Servers for use with Vimspector.', + epilog = + """ + If you're not sure, normally --all is enough to get started. + Custom server definitions can be defined in JSON files, allowing + installation of arbitrary servers packaged in one of the ways that this + installer understands. -def MakeExecutable( file_path ): - # TODO: import stat and use them by _just_ adding the X bit. - print( 'Making executable: {}'.format( file_path ) ) - os.chmod( file_path, 0o755 ) + The format of the file can be found on the Vimspector reference guide: + https://puremourning.github.io/vimspector - -def InstallCppTools( name, root ): - extension = os.path.join( root, 'extension' ) - - # It's hilarious, but the execute bits aren't set in the vsix. So they - # actually have javascript code which does this. It's just a horrible horrible - # hack that really is not funny. - MakeExecutable( os.path.join( extension, 'debugAdapters', 'OpenDebugAD7' ) ) - with open( os.path.join( extension, 'package.json' ) ) as f: - package = json.load( f ) - runtime_dependencies = package[ 'runtimeDependencies' ] - for dependency in runtime_dependencies: - for binary in dependency.get( 'binaries' ): - file_path = os.path.abspath( os.path.join( extension, binary ) ) - if os.path.exists( file_path ): - MakeExecutable( os.path.join( extension, binary ) ) - - MakeExtensionSymlink( name, root ) - - -def InstallTclProDebug( name, root ): - configure = [ './configure' ] - - if OS == 'macos': - # Apple removed the headers from system frameworks because they are - # determined to make life difficult. And the TCL configure scripts are super - # old so don't know about this. So we do their job for them and try and find - # a tclConfig.sh. - # - # NOTE however that in Apple's infinite wisdom, installing the "headers" in - # the other location is actually broken because the paths in the - # tclConfig.sh are pointing at the _old_ location. You actually do have to - # run the package installation which puts the headers back in order to work. - # This is why the below list is does not contain stuff from - # /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform - # '/Applications/Xcode.app/Contents/Developer/Platforms' - # '/MacOSX.platform/Developer/SDKs/MacOSX.sdk/System' - # '/Library/Frameworks/Tcl.framework', - # '/Applications/Xcode.app/Contents/Developer/Platforms' - # '/MacOSX.platform/Developer/SDKs/MacOSX.sdk/System' - # '/Library/Frameworks/Tcl.framework/Versions' - # '/Current', - for p in [ '/usr/local/opt/tcl-tk/lib' ]: - if os.path.exists( os.path.join( p, 'tclConfig.sh' ) ): - configure.append( '--with-tcl=' + p ) - break - - - with CurrentWorkingDir( os.path.join( root, 'lib', 'tclparser' ) ): - subprocess.check_call( configure ) - subprocess.check_call( [ 'make' ] ) - - MakeSymlink( gadget_dir, name, root ) - - -def InstallNodeDebug( name, root ): - node_version = subprocess.check_output( [ 'node', '--version' ], - universal_newlines=True ).strip() - print( "Node.js version: {}".format( node_version ) ) - if list( map( int, node_version[ 1: ].split( '.' ) ) ) >= [ 12, 0, 0 ]: - print( "Can't install vscode-debug-node2:" ) - print( "Sorry, you appear to be running node 12 or later. That's not " - "compatible with the build system for this extension, and as far as " - "we know, there isn't a pre-built independent package." ) - print( "My advice is to install nvm, then do:" ) - print( " $ nvm install --lts 10" ) - print( " $ nvm use --lts 10" ) - print( " $ ./install_gadget.py --enable-node ..." ) - raise RuntimeError( 'Invalid node environent for node debugger' ) - - with CurrentWorkingDir( root ): - subprocess.check_call( [ 'npm', 'install' ] ) - subprocess.check_call( [ 'npm', 'run', 'build' ] ) - MakeSymlink( gadget_dir, name, root ) - - -def WithRetry( f ): - retries = 5 - timeout = 1 # seconds - - @functools.wraps( f ) - def wrapper( *args, **kwargs ): - thrown = None - for _ in range( retries ): - try: - return f( *args, **kwargs ) - except Exception as e: - thrown = e - print( "Failed - {}, will retry in {} seconds".format( e, timeout ) ) - time.sleep( timeout ) - raise thrown - - return wrapper - - -@WithRetry -def UrlOpen( *args, **kwargs ): - return urllib2.urlopen( *args, **kwargs ) - - -def DownloadFileTo( url, destination, file_name = None, checksum = None ): - if not file_name: - file_name = url.split( '/' )[ -1 ] - - file_path = os.path.abspath( os.path.join( destination, file_name ) ) - - if not os.path.isdir( destination ): - os.makedirs( destination ) - - if os.path.exists( file_path ): - if checksum: - if ValidateCheckSumSHA256( file_path, checksum ): - print( "Checksum matches for {}, using it".format( file_path ) ) - return file_path - else: - print( "Checksum doesn't match for {}, removing it".format( - file_path ) ) - - print( "Removing existing {}".format( file_path ) ) - os.remove( file_path ) - - r = urllib2.Request( url, headers = { 'User-Agent': 'Vimspector' } ) - - print( "Downloading {} to {}/{}".format( url, destination, file_name ) ) - - with contextlib.closing( UrlOpen( r ) ) as u: - with open( file_path, 'wb' ) as f: - f.write( u.read() ) - - if checksum: - if not ValidateCheckSumSHA256( file_path, checksum ): - raise RuntimeError( - 'Checksum for {} ({}) does not match expected {}'.format( - file_path, - GetChecksumSHA254( file_path ), - checksum ) ) - else: - print( "Checksum for {}: {}".format( file_path, - GetChecksumSHA254( file_path ) ) ) - - return file_path - - -def GetChecksumSHA254( file_path ): - with open( file_path, 'rb' ) as existing_file: - return hashlib.sha256( existing_file.read() ).hexdigest() - - -def ValidateCheckSumSHA256( file_path, checksum ): - existing_sha256 = GetChecksumSHA254( file_path ) - return existing_sha256 == checksum - - -def RemoveIfExists( destination ): - if os.path.exists( destination ) or os.path.islink( destination ): - if os.path.islink( destination ): - print( "Removing file {}".format( destination ) ) - os.remove( destination ) - else: - print( "Removing dir {}".format( destination ) ) - shutil.rmtree( destination ) - - -# Python's ZipFile module strips execute bits from files, for no good reason -# other than crappy code. Let's do it's job for it. -class ModePreservingZipFile( zipfile.ZipFile ): - def extract( self, member, path = None, pwd = None ): - if not isinstance( member, zipfile.ZipInfo ): - member = self.getinfo( member ) - - if path is None: - path = os.getcwd() - - ret_val = self._extract_member( member, path, pwd ) - attr = member.external_attr >> 16 - os.chmod( ret_val, attr ) - return ret_val - - -def ExtractZipTo( file_path, destination, format ): - print( "Extracting {} to {}".format( file_path, destination ) ) - RemoveIfExists( destination ) - - if format == 'zip': - with ModePreservingZipFile( file_path ) as f: - f.extractall( path = destination ) - elif format == 'zip.gz': - with gzip.open( file_path, 'rb' ) as f: - file_contents = f.read() - - with ModePreservingZipFile( BytesIO( file_contents ) ) as f: - f.extractall( path = destination ) - - elif format == 'tar': - try: - with tarfile.open( file_path ) as f: - f.extractall( path = destination ) - except Exception: - # There seems to a bug in python's tarfile that means it can't read some - # windows-generated tar files - os.makedirs( destination ) - with CurrentWorkingDir( destination ): - subprocess.check_call( [ 'tar', 'zxvf', file_path ] ) - - -def MakeExtensionSymlink( name, root ): - MakeSymlink( gadget_dir, name, os.path.join( root, 'extension' ) ), - - -def MakeSymlink( in_folder, link, pointing_to ): - RemoveIfExists( os.path.join( in_folder, link ) ) - - in_folder = os.path.abspath( in_folder ) - pointing_to = os.path.relpath( os.path.abspath( pointing_to ), - in_folder ) - os.symlink( pointing_to, os.path.join( in_folder, link ) ) - - -def CloneRepoTo( url, ref, destination ): - RemoveIfExists( destination ) - subprocess.check_call( [ 'git', 'clone', url, destination ] ) - subprocess.check_call( [ 'git', '-C', destination, 'checkout', ref ] ) - subprocess.check_call( [ 'git', 'submodule', 'sync', '--recursive' ] ) - subprocess.check_call( [ 'git', - 'submodule', - 'update', - '--init', - '--recursive' ] ) - - -OS = install.GetOS() -gadget_dir = install.GetGadgetDir( os.path.dirname( __file__ ), OS ) - -print( 'OS = ' + OS ) -print( 'gadget_dir = ' + gadget_dir ) - -parser = argparse.ArgumentParser() + NOTE: This script should usually _not_ be run under `sudo` or as root. It + downloads and extracts things only to directories under its own path. No + system files or folders are chnaged by this script. If you really want to + run under sudo, pass --sudo, but this is _almost certainly_ the wrong thing + to do. + """ +) parser.add_argument( '--all', action = 'store_true', help = 'Enable all supported completers' ) @@ -523,103 +69,186 @@ parser.add_argument( '--force-all', action = 'store_true', help = 'Enable all unsupported completers' ) -done_languages = set() -for name, gadget in GADGETS.items(): - lang = gadget[ 'language' ] - if lang in done_languages: - continue +parser.add_argument( '--upgrade', + action = 'store_true', + help = 'Only update adapters changed from the manifest' ) + +parser.add_argument( '--quiet', + action = 'store_true', + help = 'Suppress installation output' ) + +parser.add_argument( '--verbose', + action = 'store_true', + help = 'Force installation output' ) + +parser.add_argument( '--basedir', + action = 'store', + help = 'Advanced option. ' + 'Base directory under which to keep gadgets, ' + 'configurations, etc.. Default: vimspector ' + 'installation dir. Useful for developers or ' + 'multi-user installations' ) + +parser.add_argument( '--no-gadget-config', + action = 'store_true', + help = "Don't write the .gagets.json, just install" ) + +parser.add_argument( '--update-gadget-config', + action = 'store_true', + help = + "Update the gadget config rather than overwrite it" ) + +parser.add_argument( '--enable-custom', + dest='custom_gadget_file', + action='append', + nargs='*', + default = [], + help = 'Read custom gadget from supplied file. This ' + 'can be supplied multiple times and each time ' + 'multiple files can be passed.' ) + +parser.add_argument( '--sudo', + action='store_true', + help = "If you're really really really sure you want to " + "run this as root via sudo, pass this flag." ) + +done_languages = set() +for name, gadget in gadgets.GADGETS.items(): + langs = gadget[ 'language' ] + if not isinstance( langs, list ): + langs = [ langs ] + for lang in langs: + if lang in done_languages: + continue + + done_languages.add( lang ) + if not gadget.get( 'enabled', True ): + parser.add_argument( + '--force-enable-' + lang, + action = 'store_true', + help = 'Install the unsupported {} debug adapter for {} support'.format( + name, + lang ) ) + continue - done_languages.add( lang ) - if not gadget.get( 'enabled', True ): parser.add_argument( - '--force-enable-' + lang, + '--enable-' + lang, action = 'store_true', - help = 'Install the unsupported {} debug adapter for {} support'.format( + help = 'Install the {} debug adapter for {} support'.format( name, lang ) ) - continue - parser.add_argument( - '--enable-' + lang, - action = 'store_true', - help = 'Install the {} debug adapter for {} support'.format( - name, - lang ) ) + parser.add_argument( + '--disable-' + lang, + action = 'store_true', + help = "Don't install the {} debug adapter for {} support " + '(when supplying --all)'.format( name, lang ) ) - parser.add_argument( - '--disable-' + lang, - action = 'store_true', - help = "Don't install the {} debug adapter for {} support " - '(when supplying --all)'.format( name, lang ) ) +parser.add_argument( + "--no-check-certificate", + action = "store_true", + help = "Do not verify SSL certificates for file downloads." +) args = parser.parse_args() +installer.AbortIfSUperUser( args.sudo ) + +vimspector_base = os.path.dirname( __file__ ) +if args.basedir: + vimspector_base = os.path.abspath( args.basedir ) + +install.MakeInstallDirs( vimspector_base ) +installer.Configure( vimspector_base = vimspector_base, + quiet = args.quiet and not args.verbose, + no_check_certificate = args.no_check_certificate ) + if args.force_all and not args.all: args.all = True +CUSTOM_GADGETS = {} +custom_files = glob.glob( os.path.join( vimspector_base, + 'gadgets', + 'custom', + '*.json' ) ) +for custom_file_name in functools.reduce( operator.add, + args.custom_gadget_file, + custom_files ): + with open( custom_file_name, 'r' ) as custom_file: + CUSTOM_GADGETS.update( json.loads( minify( custom_file.read() ) ) ) + + failed = [] -all_adapters = {} -for name, gadget in GADGETS.items(): - if not gadget.get( 'enabled', True ): - if ( not args.force_all - and not getattr( args, 'force_enable_' + gadget[ 'language' ] ) ): - continue - else: - if not args.all and not getattr( args, 'enable_' + gadget[ 'language' ] ): - continue - if getattr( args, 'disable_' + gadget[ 'language' ] ): - continue +succeeded = [] +all_adapters = installer.ReadAdapters( + read_existing = args.update_gadget_config ) +manifest = installer.Manifest() - try: - v = {} - v.update( gadget.get( 'all', {} ) ) - v.update( gadget.get( OS, {} ) ) - - if 'download' in gadget: - if 'file_name' not in v: - raise RuntimeError( "Unsupported OS {} for gadget {}".format( OS, - name ) ) - - destination = os.path.join( gadget_dir, 'download', name, v[ 'version' ] ) - - url = string.Template( gadget[ 'download' ][ 'url' ] ).substitute( v ) - - file_path = DownloadFileTo( - url, - destination, - file_name = gadget[ 'download' ].get( 'target' ), - checksum = v.get( 'checksum' ) ) - root = os.path.join( destination, 'root' ) - ExtractZipTo( file_path, - root, - format = gadget[ 'download' ].get( 'format', 'zip' ) ) - elif 'repo' in gadget: - url = string.Template( gadget[ 'repo' ][ 'url' ] ).substitute( v ) - ref = string.Template( gadget[ 'repo' ][ 'ref' ] ).substitute( v ) - - destination = os.path.join( gadget_dir, 'download', name ) - CloneRepoTo( url, ref, destination ) - root = destination - - if 'do' in gadget: - gadget[ 'do' ]( name, root ) +for name, gadget in gadgets.GADGETS.items(): + langs = gadget[ 'language' ] + if not isinstance( langs, list ): + langs = [ langs ] + skip = 0 + for lang in langs: + if not gadget.get( 'enabled', True ): + if ( not args.force_all + and not getattr( args, 'force_enable_' + lang ) ): + skip = skip + 1 + continue else: - MakeExtensionSymlink( name, root ) + if not args.all and not getattr( args, 'enable_' + lang ): + skip = skip + 1 + continue + if getattr( args, 'disable_' + lang ): + skip = skip + 1 + continue + if skip == len( langs ): + continue - all_adapters.update( gadget.get( 'adapters', {} ) ) + if not args.upgrade: + manifest.Clear( name ) + + installer.InstallGagdet( name, + gadget, + manifest, + succeeded, + failed, + all_adapters ) - print( "Done installing {}".format( name ) ) - except Exception as e: - traceback.print_exc() - failed.append( name ) - print( "FAILED installing {}: {}".format( name, e ) ) +for name, gadget in CUSTOM_GADGETS.items(): + if not args.upgrade: + manifest.Clear( name ) + installer.InstallGagdet( name, + gadget, + manifest, + succeeded, + failed, + all_adapters ) -with open( install.GetGadgetConfigFile( os.path.dirname( __file__ ) ), - 'w' ) as f: - json.dump( { 'adapters': all_adapters }, f, indent=2, sort_keys=True ) +if args.no_gadget_config: + print( "" ) + print( "Would write the following gadgets: " ) + installer.WriteAdapters( all_adapters, to_file = sys.stdout ) +else: + installer.WriteAdapters( all_adapters ) + +manifest.Write() + +if args.basedir: + print( "" ) + print( "***NOTE***: You set --basedir to " + args.basedir + + ". Therefore you _must_ ensure this is in your vimrc:\n" + "let g:vimspector_base_dir='" + vimspector_base + "'" ) + +if succeeded: + print( "Done" ) + print( "The following adapters were installed successfully:\n - {}".format( + '\n - '.join( succeeded ) ) ) if failed: - raise RuntimeError( 'Failed to install gadgets: {}'.format( - ','.join( failed ) ) ) + sys.exit( 'Failed to install adapters:\n * {}{}'.format( + '\n * '.join( failed ), + "\nRe-run with --verbose for more info on failures" + if args.quiet and not args.verbose else '' ) ) diff --git a/make_package b/make_package index 2401fce..1b34a05 100755 --- a/make_package +++ b/make_package @@ -16,13 +16,20 @@ mkdir -p ${PACK} pushd ${PACK} mkdir -p vimspector/opt/vimspector pushd vimspector/opt/vimspector - for d in autoload plugin python3 vendor doc; do + for d in autoload plugin python3 vendor doc support; do if [[ -d ${ROOT}/$d ]]; then cp -r ${ROOT}/$d . fi done mkdir -p gadgets cp -r ${ROOT}/gadgets/${OS} gadgets/ + for f in install_gadget.py \ + CODE_OF_CONDUCT.md \ + CONTRIBUTING.md \ + LICENCE \ + README.md; do + cp ${ROOT}/${f} . + done popd popd diff --git a/plugin/vimspector.vim b/plugin/vimspector.vim index 13ffa3b..2668bf1 100644 --- a/plugin/vimspector.vim +++ b/plugin/vimspector.vim @@ -13,6 +13,13 @@ " See the License for the specific language governing permissions and " limitations under the License. +if !has( 'python3' ) + echohl WarningMsg + echom 'Vimspector unavailable: Requires Vim compiled with +python3' + echohl None + finish +endif + " Boilerplate {{{ let s:save_cpo = &cpoptions set cpoptions&vim @@ -26,38 +33,127 @@ if exists( 'g:loaded_vimpector' ) call s:restore_cpo() finish endif - -" TODO: -" - Check Vim version (for jobs) -" - Check python support -" - Add commands/mappings/menus? +"}}} let g:loaded_vimpector = 1 +let g:vimspector_home = expand( ':p:h:h' ) let s:mappings = get( g:, 'vimspector_enable_mappings', '' ) +nnoremap VimspectorContinue + \ :call vimspector#Continue() +nnoremap VimspectorLaunch + \ :call vimspector#Launch( v:true ) +nnoremap VimspectorStop + \ :call vimspector#Stop() +nnoremap VimspectorRestart + \ :call vimspector#Restart() +nnoremap VimspectorPause + \ :call vimspector#Pause() +nnoremap VimspectorToggleBreakpoint + \ :call vimspector#ToggleBreakpoint() +nnoremap VimspectorToggleConditionalBreakpoint + \ :call vimspector#ToggleBreakpoint( + \ { 'condition': input( 'Enter condition expression: ' ), + \ 'hitCondition': input( 'Enter hit count expression: ' ) } + \ ) +nnoremap VimspectorAddFunctionBreakpoint + \ :call vimspector#AddFunctionBreakpoint( expand( '' ) ) +nnoremap VimspectorStepOver + \ :call vimspector#StepOver() +nnoremap VimspectorStepInto + \ :call vimspector#StepInto() +nnoremap VimspectorStepOut + \ :call vimspector#StepOut() + +nnoremap VimspectorRunToCursor + \ :call vimspector#RunToCursor() + +" Eval for normal mode +nnoremap VimspectorBalloonEval + \ :call vimspector#ShowEvalBalloon( 0 ) +" And for visual modes +xnoremap VimspectorBalloonEval + \ :call vimspector#ShowEvalBalloon( 1 ) + +nnoremap VimspectorUpFrame + \ :call vimspector#UpFrame() +nnoremap VimspectorDownFrame + \ :call vimspector#DownFrame() + if s:mappings ==# 'VISUAL_STUDIO' - nnoremap :call vimspector#Continue() - nnoremap :call vimspector#Stop() - nnoremap :call vimspector#Restart() - nnoremap :call vimspector#Pause() - nnoremap :call vimspector#ToggleBreakpoint() - nnoremap :call vimspector#AddFunctionBreakpoint( expand( '' ) ) - nnoremap :call vimspector#StepOver() - nnoremap :call vimspector#StepInto() - nnoremap :call vimspector#StepOut() + nmap VimspectorContinue + nmap VimspectorStop + nmap VimspectorRestart + nmap VimspectorPause + nmap VimspectorToggleBreakpoint + nmap VimspectorAddFunctionBreakpoint + nmap VimspectorStepOver + nmap VimspectorStepInto + nmap VimspectorStepOut elseif s:mappings ==# 'HUMAN' - nnoremap :call vimspector#Continue() - nnoremap :call vimspector#Stop() - nnoremap :call vimspector#Restart() - nnoremap :call vimspector#Pause() - nnoremap :call vimspector#ToggleBreakpoint() - nnoremap :call vimspector#AddFunctionBreakpoint( expand( '' ) ) - nnoremap :call vimspector#StepOver() - nnoremap :call vimspector#StepInto() - nnoremap :call vimspector#StepOut() + nmap VimspectorContinue + nmap VimspectorLaunch + nmap VimspectorStop + nmap VimspectorRestart + nmap VimspectorPause + nmap VimspectorToggleBreakpoint + nmap VimspectorToggleConditionalBreakpoint + nmap VimspectorAddFunctionBreakpoint + nmap VimspectorRunToCursor + nmap VimspectorStepOver + nmap VimspectorStepInto + nmap VimspectorStepOut endif -"}}} + +command! -bar -nargs=1 -complete=custom,vimspector#CompleteExpr + \ VimspectorWatch + \ call vimspector#AddWatch( ) +command! -bar -nargs=? -complete=custom,vimspector#CompleteOutput + \ VimspectorShowOutput + \ call vimspector#ShowOutput( ) +command! -bar + \ VimspectorToggleLog + \ call vimspector#ToggleLog() +command! -bar + \ VimspectorDebugInfo + \ call vimspector#PrintDebugInfo() +command! -nargs=1 -complete=custom,vimspector#CompleteExpr + \ VimspectorEval + \ call vimspector#Evaluate( ) +command! -bar + \ VimspectorReset + \ call vimspector#Reset( { 'interactive': v:true } ) + +" Installer commands +command! -bar -bang -nargs=* -complete=custom,vimspector#CompleteInstall + \ VimspectorInstall + \ call vimspector#Install( , ) +command! -bar -bang -nargs=* + \ VimspectorUpdate + \ call vimspector#Update( , ) +command! -bar -nargs=0 + \ VimspectorAbortInstall + \ call vimspector#AbortInstall() + +" Dummy autocommands so that we can call this whenever +augroup VimspectorUserAutoCmds + autocmd! + autocmd User VimspectorUICreated silent + autocmd User VimspectorTerminalOpened silent + autocmd user VimspectorJumpedToFrame silent + autocmd user VimspectorDebugEnded silent +augroup END + +" FIXME: Only register this _while_ debugging is active +augroup Vimspector + autocmd! + autocmd BufNew * call vimspector#OnBufferCreated( expand( '' ) ) +augroup END + +" boilerplate {{{ call s:restore_cpo() +" }}} + diff --git a/python3/vimspector/breakpoints.py b/python3/vimspector/breakpoints.py index e511b09..7aa50ce 100644 --- a/python3/vimspector/breakpoints.py +++ b/python3/vimspector/breakpoints.py @@ -21,7 +21,7 @@ import os import logging import json -from vimspector import utils +from vimspector import utils, signs class ServerBreakpointHandler( object ): @@ -44,6 +44,7 @@ class ProjectBreakpoints( object ): self._line_breakpoints = defaultdict( list ) self._func_breakpoints = [] self._exception_breakpoints = None + self._configured_breakpoints = {} # FIXME: Remove this. Remove breakpoints nonesense from code.py self._breakpoints_handler = None @@ -51,9 +52,23 @@ class ProjectBreakpoints( object ): self._next_sign_id = 1 - # TODO: Change to sign_define ? - vim.command( 'sign define vimspectorBP text==> texthl=Error' ) - vim.command( 'sign define vimspectorBPDisabled text=!> texthl=Warning' ) + if not signs.SignDefined( 'vimspectorBP' ): + signs.DefineSign( 'vimspectorBP', + text = '●', + double_text = '●', + texthl = 'WarningMsg' ) + + if not signs.SignDefined( 'vimspectorBPCond' ): + signs.DefineSign( 'vimspectorBPCond', + text = '◆', + double_text = '◆', + texthl = 'WarningMsg' ) + + if not signs.SignDefined( 'vimspectorBPDisabled' ): + signs.DefineSign( 'vimspectorBPDisabled', + text = '●', + double_text = '●', + texthl = 'LineNr' ) def ConnectionUp( self, connection ): @@ -73,8 +88,10 @@ class ProjectBreakpoints( object ): # NOTE: we don't reset self._exception_breakpoints because we don't want to # re-ask the user every time for the sane info. + # FIXME: If the adapter type changes, we should probably forget this ? - def ListBreakpoints( self ): + + def BreakpointsAsQuickFix( self ): # FIXME: Handling of breakpoints is a mess, split between _codeView and this # object. This makes no sense and should be centralised so that we don't # have this duplication and bug factory. @@ -84,14 +101,16 @@ class ProjectBreakpoints( object ): else: for file_name, breakpoints in self._line_breakpoints.items(): for bp in breakpoints: + self._SignToLine( file_name, bp ) qf.append( { 'filename': file_name, 'lnum': bp[ 'line' ], 'col': 1, 'type': 'L', 'valid': 1 if bp[ 'state' ] == 'ENABLED' else 0, - 'text': "Line breakpoint - {}".format( - bp[ 'state' ] ) + 'text': "Line breakpoint - {}: {}".format( + bp[ 'state' ], + json.dumps( bp[ 'options' ] ) ) } ) # I think this shows that the qf list is not right for this. for bp in self._func_breakpoints: @@ -101,71 +120,152 @@ class ProjectBreakpoints( object ): 'col': 1, 'type': 'F', 'valid': 1, - 'text': "Function breakpoint: {}".format( bp[ 'function' ] ), + 'text': "Function breakpoint: {}: {}".format( bp[ 'function' ], + bp[ 'options' ] ), } ) - vim.eval( 'setqflist( {} )'.format( json.dumps( qf ) ) ) + return qf + def ClearBreakpoints( self ): # These are the user-entered breakpoints. for file_name, breakpoints in self._line_breakpoints.items(): for bp in breakpoints: + self._SignToLine( file_name, bp ) if 'sign_id' in bp: - vim.command( 'sign unplace {0} group=VimspectorBP'.format( - bp[ 'sign_id' ] ) ) + signs.UnplaceSign( bp[ 'sign_id' ], 'VimspectorBP' ) self._line_breakpoints = defaultdict( list ) self._func_breakpoints = [] + self._exception_breakpoints = None self.UpdateUI() - def ToggleBreakpoint( self ): - line, column = vim.current.window.cursor + def _FindLineBreakpoint( self, file_name, line ): + file_name = os.path.abspath( file_name ) + for index, bp in enumerate( self._line_breakpoints[ file_name ] ): + self._SignToLine( file_name, bp ) + if bp[ 'line' ] == line: + return bp, index + + return None, None + + + def _PutLineBreakpoint( self, file_name, line, options ): + self._line_breakpoints[ os.path.abspath( file_name ) ].append( { + 'state': 'ENABLED', + 'line': line, + 'options': options, + # 'sign_id': , + # + # Used by other breakpoint types (specified in options): + # 'condition': ..., + # 'hitCondition': ..., + # 'logMessage': ... + } ) + + + def _DeleteLineBreakpoint( self, bp, file_name, index ): + if 'sign_id' in bp: + signs.UnplaceSign( bp[ 'sign_id' ], 'VimspectorBP' ) + del self._line_breakpoints[ os.path.abspath( file_name ) ][ index ] + + + def ToggleBreakpoint( self, options ): + line, _ = vim.current.window.cursor file_name = vim.current.buffer.name if not file_name: return - found_bp = False - action = 'New' - for index, bp in enumerate( self._line_breakpoints[ file_name ] ): - if bp[ 'line' ] == line: - found_bp = True - if bp[ 'state' ] == 'ENABLED' and not self._connection: - bp[ 'state' ] = 'DISABLED' - action = 'Disable' - else: - if 'sign_id' in bp: - vim.command( 'sign unplace {0} group=VimspectorBP'.format( - bp[ 'sign_id' ] ) ) - del self._line_breakpoints[ file_name ][ index ] - action = 'Delete' - break - - self._logger.debug( "Toggle found bp at {}:{} ? {} ({})".format( - file_name, - line, - found_bp, - action ) ) - - if not found_bp: - self._line_breakpoints[ file_name ].append( { - 'state': 'ENABLED', - 'line': line, - # 'sign_id': , - # - # Used by other breakpoint types: - # 'condition': ..., - # 'hitCondition': ..., - # 'logMessage': ... - } ) + bp, index = self._FindLineBreakpoint( file_name, line ) + if bp is None: + # ADD + self._PutLineBreakpoint( file_name, line, options ) + elif bp[ 'state' ] == 'ENABLED' and not self._connection: + # DISABLE + bp[ 'state' ] = 'DISABLED' + else: + # DELETE + self._DeleteLineBreakpoint( bp, file_name, index ) self.UpdateUI() - def AddFunctionBreakpoint( self, function ): + + def SetLineBreakpoint( self, file_name, line_num, options, then = None ): + bp, _ = self._FindLineBreakpoint( file_name, line_num ) + if bp is not None: + bp[ 'options' ] = options + return + self._PutLineBreakpoint( file_name, line_num, options ) + self.UpdateUI( then ) + + + def ClearLineBreakpoint( self, file_name, line_num ): + bp, index = self._FindLineBreakpoint( file_name, line_num ) + if bp is None: + return + self._DeleteLineBreakpoint( bp, file_name, index ) + self.UpdateUI() + + + def ClearTemporaryBreakpoint( self, file_name, line_num ): + bp, index = self._FindLineBreakpoint( file_name, line_num ) + if bp is None: + return + if bp[ 'options' ].get( 'temporary' ): + self._DeleteLineBreakpoint( bp, file_name, index ) + self.UpdateUI() + + + def ClearTemporaryBreakpoints( self ): + to_delete = [] + for file_name, breakpoints in self._line_breakpoints.items(): + for index, bp in enumerate( breakpoints ): + if bp[ 'options' ].get( 'temporary' ): + to_delete.append( ( bp, file_name, index ) ) + + for entry in to_delete: + self._DeleteLineBreakpoint( *entry ) + + + def _UpdateTemporaryBreakpoints( self, breakpoints, temp_idxs ): + # adjust any temporary breakpoints to match the server result + # TODO: Maybe now is the time to ditch the split breakpoints nonesense + for temp_idx, user_bp in temp_idxs: + if temp_idx >= len( breakpoints ): + # Just can't trust servers ? + self._logger.debug( "Server Error - invalid breakpoints list did not " + "contain entry for temporary breakpoint at index " + f"{ temp_idx } i.e. { user_bp }" ) + continue + + bp = breakpoints[ temp_idx ] + + if 'line' not in bp or not bp[ 'verified' ]: + utils.UserMessage( + "Unable to set temporary breakpoint at line " + f"{ user_bp[ 'line' ] } execution will continue...", + persist = True, + error = True ) + + self._logger.debug( f"Updating temporary breakpoint { user_bp } line " + f"{ user_bp[ 'line' ] } to { bp[ 'line' ] }" ) + + # if it was moved, update the user-breakpoint so that we unset it + # again properly + user_bp[ 'line' ] = bp[ 'line' ] + + + + def AddFunctionBreakpoint( self, function, options ): self._func_breakpoints.append( { - 'state': 'ENABLED', - 'function': function, + 'state': 'ENABLED', + 'function': function, + 'options': options, + # Specified in options: + # 'condition': ..., + # 'hitCondition': ..., } ) # TODO: We don't really have aanything to update here, but if we're going to @@ -173,11 +273,13 @@ class ProjectBreakpoints( object ): self.UpdateUI() - def UpdateUI( self ): + def UpdateUI( self, then = None ): if self._connection: - self.SendBreakpoints() + self.SendBreakpoints( then ) else: self._ShowBreakpoints() + if then: + then() def SetBreakpointsHandler( self, handler ): @@ -185,6 +287,10 @@ class ProjectBreakpoints( object ): self._breakpoints_handler = handler + def SetConfiguredBreakpoints( self, configured_breakpoints ): + self._configured_breakpoints = configured_breakpoints + + def SendBreakpoints( self, doneHandler = None ): assert self._breakpoints_handler is not None @@ -193,27 +299,59 @@ class ProjectBreakpoints( object ): awaiting = 0 - def response_handler( source, msg ): - if msg: - self._breakpoints_handler.AddBreakpoints( source, msg ) + def response_received( *failure_args ): nonlocal awaiting awaiting = awaiting - 1 + + if failure_args and self._connection: + reason, msg = failure_args + utils.UserMessage( 'Unable to set breakpoint: {0}'.format( reason ), + persist = True, + error = True ) + if awaiting == 0 and doneHandler: doneHandler() + def response_handler( source, msg, temp_idxs = [] ): + if msg: + self._breakpoints_handler.AddBreakpoints( source, msg ) + + breakpoints = ( msg.get( 'body' ) or {} ).get( 'breakpoints' ) or [] + self._UpdateTemporaryBreakpoints( breakpoints, temp_idxs ) + response_received() + + + # NOTE: Must do this _first_ otherwise we might send requests and get + # replies before we finished sending all the requests. + if self._exception_breakpoints is None: + self._SetUpExceptionBreakpoints( self._configured_breakpoints ) + + + # TODO: add the _configured_breakpoints to line_breakpoints for file_name, line_breakpoints in self._line_breakpoints.items(): + temp_idxs = [] breakpoints = [] for bp in line_breakpoints: + self._SignToLine( file_name, bp ) if 'sign_id' in bp: - vim.command( 'sign unplace {0} group=VimspectorBP'.format( - bp[ 'sign_id' ] ) ) + signs.UnplaceSign( bp[ 'sign_id' ], 'VimspectorBP' ) del bp[ 'sign_id' ] if bp[ 'state' ] != 'ENABLED': continue - breakpoints.append( { 'line': bp[ 'line' ] } ) + dap_bp = {} + dap_bp.update( bp[ 'options' ] ) + dap_bp.update( { 'line': bp[ 'line' ] } ) + + dap_bp.pop( 'temporary', None ) + + if bp[ 'options' ].get( 'temporary' ): + temp_idxs.append( [ len( breakpoints ), bp ] ) + + breakpoints.append( dap_bp ) + source = { 'name': os.path.basename( file_name ), @@ -222,7 +360,13 @@ class ProjectBreakpoints( object ): awaiting = awaiting + 1 self._connection.DoRequest( - lambda msg: response_handler( source, msg ), + # The source=source here is critical to ensure that we capture each + # source in the iteration, rather than ending up passing the same source + # to each callback. + lambda msg, source=source, temp_idxs=temp_idxs: response_handler( + source, + msg, + temp_idxs = temp_idxs ), { 'command': 'setBreakpoints', 'arguments': { @@ -230,27 +374,34 @@ class ProjectBreakpoints( object ): 'breakpoints': breakpoints, }, 'sourceModified': False, # TODO: We can actually check this - } + }, + failure_handler = response_received ) + # TODO: Add the _configured_breakpoints to function breakpoints + if self._server_capabilities.get( 'supportsFunctionBreakpoints' ): awaiting = awaiting + 1 + breakpoints = [] + for bp in self._func_breakpoints: + if bp[ 'state' ] != 'ENABLED': + continue + dap_bp = {} + dap_bp.update( bp[ 'options' ] ) + dap_bp.update( { 'name': bp[ 'function' ] } ) + breakpoints.append( dap_bp ) + self._connection.DoRequest( lambda msg: response_handler( None, msg ), { 'command': 'setFunctionBreakpoints', 'arguments': { - 'breakpoints': [ - { 'name': bp[ 'function' ] } - for bp in self._func_breakpoints if bp[ 'state' ] == 'ENABLED' - ], + 'breakpoints': breakpoints, } - } + }, + failure_handler = response_received ) - if self._exception_breakpoints is None: - self._SetUpExceptionBreakpoints() - if self._exception_breakpoints: awaiting = awaiting + 1 self._connection.DoRequest( @@ -258,14 +409,15 @@ class ProjectBreakpoints( object ): { 'command': 'setExceptionBreakpoints', 'arguments': self._exception_breakpoints - } + }, + failure_handler = response_received ) if awaiting == 0 and doneHandler: doneHandler() - def _SetUpExceptionBreakpoints( self ): + def _SetUpExceptionBreakpoints( self, configured_breakpoints ): exception_breakpoint_filters = self._server_capabilities.get( 'exceptionBreakpointFilters', [] ) @@ -278,14 +430,27 @@ class ProjectBreakpoints( object ): # trigger requesting threads etc.). See the note in # debug_session.py:_Initialise for more detials exception_filters = [] + configured_filter_options = configured_breakpoints.get( 'exception', {} ) if exception_breakpoint_filters: for f in exception_breakpoint_filters: default_value = 'Y' if f.get( 'default' ) else 'N' - result = utils.AskForInput( - "Break on {} (Y/N/default: {})? ".format( f[ 'label' ], - default_value ), - default_value ) + if f[ 'filter' ] in configured_filter_options: + result = configured_filter_options[ f[ 'filter' ] ] + + if isinstance( result, bool ): + result = 'Y' if result else 'N' + + if not isinstance( result, str ) or result not in ( 'Y', 'N', '' ): + raise ValueError( + f"Invalid value for exception breakpoint filter '{f}': " + f"'{result}'. Must be boolean, 'Y', 'N' or '' (default)" ) + else: + result = utils.AskForInput( + "{}: Break on {} (Y/N/default: {})? ".format( f[ 'filter' ], + f[ 'label' ], + default_value ), + default_value ) if result == 'Y': exception_filters.append( f[ 'filter' ] ) @@ -302,20 +467,46 @@ class ProjectBreakpoints( object ): # pay any attention to them anyway. self._exception_breakpoints[ 'exceptionOptions' ] = [] + + def Refresh( self, file_name ): + # TODO: Just this file ? + self._ShowBreakpoints() + + def _ShowBreakpoints( self ): for file_name, line_breakpoints in self._line_breakpoints.items(): for bp in line_breakpoints: + self._SignToLine( file_name, bp ) if 'sign_id' in bp: - vim.command( 'sign unplace {0} group=VimspectorBP '.format( - bp[ 'sign_id' ] ) ) + signs.UnplaceSign( bp[ 'sign_id' ], 'VimspectorBP' ) else: bp[ 'sign_id' ] = self._next_sign_id self._next_sign_id += 1 - vim.command( - 'sign place {0} group=VimspectorBP line={1} name={2} file={3}'.format( - bp[ 'sign_id' ] , - bp[ 'line' ], - 'vimspectorBP' if bp[ 'state' ] == 'ENABLED' - else 'vimspectorBPDisabled', - file_name ) ) + sign = ( 'vimspectorBPDisabled' if bp[ 'state' ] != 'ENABLED' + else 'vimspectorBPCond' if 'condition' in bp[ 'options' ] + else 'vimspectorBP' ) + + if utils.BufferExists( file_name ): + signs.PlaceSign( bp[ 'sign_id' ], + 'VimspectorBP', + sign, + file_name, + bp[ 'line' ] ) + + + def _SignToLine( self, file_name, bp ): + if 'sign_id' not in bp: + return bp[ 'line' ] + + if not utils.BufferExists( file_name ): + return bp[ 'line' ] + + signs = vim.eval( "sign_getplaced( '{}', {} )".format( + utils.Escape( file_name ), + json.dumps( { 'id': bp[ 'sign_id' ], 'group': 'VimspectorBP', } ) ) ) + + if len( signs ) == 1 and len( signs[ 0 ][ 'signs' ] ) == 1: + bp[ 'line' ] = int( signs[ 0 ][ 'signs' ][ 0 ][ 'lnum' ] ) + + return bp[ 'line' ] diff --git a/python3/vimspector/code.py b/python3/vimspector/code.py index f28a1b5..98aeca5 100644 --- a/python3/vimspector/code.py +++ b/python3/vimspector/code.py @@ -18,63 +18,133 @@ import logging import json from collections import defaultdict -from vimspector import utils +from vimspector import utils, terminal, signs class CodeView( object ): - def __init__( self, window ): + def __init__( self, window, api_prefix ): self._window = window + self._api_prefix = api_prefix - self._terminal_window = None - self._terminal_buffer_number = None + self._terminal = None + self.current_syntax = None self._logger = logging.getLogger( __name__ ) utils.SetUpLogging( self._logger ) + # FIXME: This ID is by group, so should be module scope self._next_sign_id = 1 self._breakpoints = defaultdict( list ) self._signs = { 'vimspectorPC': None, 'breakpoints': [] } + self._current_frame = None with utils.LetCurrentWindow( self._window ): - vim.command( 'nnoremenu WinBar.Continue :call vimspector#Continue()' ) - vim.command( 'nnoremenu WinBar.Next :call vimspector#StepOver()' ) - vim.command( 'nnoremenu WinBar.Step :call vimspector#StepInto()' ) - vim.command( 'nnoremenu WinBar.Finish :call vimspector#StepOut()' ) - vim.command( 'nnoremenu WinBar.Pause :call vimspector#Pause()' ) - vim.command( 'nnoremenu WinBar.Stop :call vimspector#Stop()' ) - vim.command( 'nnoremenu WinBar.Restart :call vimspector#Restart()' ) - vim.command( 'nnoremenu WinBar.Reset :call vimspector#Reset()' ) + if utils.UseWinBar(): + # Buggy neovim doesn't render correctly when the WinBar is defined: + # https://github.com/neovim/neovim/issues/12689 + vim.command( 'nnoremenu WinBar.■\\ Stop ' + ':call vimspector#Stop()' ) + vim.command( 'nnoremenu WinBar.▶\\ Cont ' + ':call vimspector#Continue()' ) + vim.command( 'nnoremenu WinBar.▷\\ Pause ' + ':call vimspector#Pause()' ) + vim.command( 'nnoremenu WinBar.↷\\ Next ' + ':call vimspector#StepOver()' ) + vim.command( 'nnoremenu WinBar.→\\ Step ' + ':call vimspector#StepInto()' ) + vim.command( 'nnoremenu WinBar.←\\ Out ' + ':call vimspector#StepOut()' ) + vim.command( 'nnoremenu WinBar.⟲: ' + ':call vimspector#Restart()' ) + vim.command( 'nnoremenu WinBar.✕ ' + ':call vimspector#Reset()' ) - vim.command( 'sign define vimspectorPC text=-> texthl=Search' ) + if not signs.SignDefined( 'vimspectorPC' ): + signs.DefineSign( 'vimspectorPC', + text = '▶', + double_text = '▶', + texthl = 'MatchParen', + linehl = 'CursorLine' ) + if not signs.SignDefined( 'vimspectorPCBP' ): + signs.DefineSign( 'vimspectorPCBP', + text = '●▶', + double_text = '▷', + texthl = 'MatchParen', + linehl = 'CursorLine' ) + + + def _UndisplayPC( self, clear_pc = True ): + if clear_pc: + self._current_frame = None + if self._signs[ 'vimspectorPC' ]: + signs.UnplaceSign( self._signs[ 'vimspectorPC' ], 'VimspectorCode' ) + self._signs[ 'vimspectorPC' ] = None + + + def _DisplayPC( self ): + frame = self._current_frame + if not frame: + return + + self._UndisplayPC( clear_pc = False ) + + # FIXME: Do we relly need to keep using up IDs ? + self._signs[ 'vimspectorPC' ] = self._next_sign_id + self._next_sign_id += 1 + + sign = 'vimspectorPC' + # If there's also a breakpoint on this line, use vimspectorPCBP + for bp in self._breakpoints.get( frame[ 'source' ][ 'path' ], [] ): + if 'line' not in bp: + continue + + if bp[ 'line' ] == frame[ 'line' ]: + sign = 'vimspectorPCBP' + break + + if utils.BufferExists( frame[ 'source' ][ 'path' ] ): + signs.PlaceSign( self._signs[ 'vimspectorPC' ], + 'VimspectorCode', + sign, + frame[ 'source' ][ 'path' ], + frame[ 'line' ] ) def SetCurrentFrame( self, frame ): - if self._signs[ 'vimspectorPC' ]: - vim.command( 'sign unplace {} group=VimspectorCode'.format( - self._signs[ 'vimspectorPC' ] ) ) - self._signs[ 'vimspectorPC' ] = None + """Returns True if the code window was updated with the frame, False + otherwise. False means either the frame is junk, we couldn't find the file + (or don't have the data) or the code window no longer exits.""" if not frame or not frame.get( 'source' ): + self._UndisplayPC() return False if 'path' not in frame[ 'source' ]: + self._UndisplayPC() + return False + + self._current_frame = frame + + if not self._window.valid: return False utils.JumpToWindow( self._window ) - try: utils.OpenFileInCurrentWindow( frame[ 'source' ][ 'path' ] ) + vim.command( 'doautocmd User VimspectorJumpedToFrame' ) except vim.error: self._logger.exception( 'Unexpected vim error opening file {}'.format( frame[ 'source' ][ 'path' ] ) ) return False # SIC: column is 0-based, line is 1-based in vim. Why? Nobody knows. + # Note: max() with 0 because some debug adapters (go) return 0 for the + # column. try: - self._window.cursor = ( frame[ 'line' ], frame[ 'column' ] - 1 ) + self._window.cursor = ( frame[ 'line' ], max( frame[ 'column' ] - 1, 0 ) ) except vim.error: self._logger.exception( "Unable to jump to %s:%s in %s, maybe the file " "doesn't exist", @@ -83,25 +153,21 @@ class CodeView( object ): frame[ 'source' ][ 'path' ] ) return False - self._signs[ 'vimspectorPC' ] = self._next_sign_id - self._next_sign_id += 1 + self.current_syntax = utils.ToUnicode( + vim.current.buffer.options[ 'syntax' ] ) - vim.command( 'sign place {0} group=VimspectorCode priority=20 ' - 'line={1} name=vimspectorPC ' - 'file={2}'.format( - self._signs[ 'vimspectorPC' ], - frame[ 'line' ], - frame[ 'source' ][ 'path' ] ) ) + self.ShowBreakpoints() return True def Clear( self ): if self._signs[ 'vimspectorPC' ]: - vim.command( 'sign unplace {} group=VimspectorCode'.format( - self._signs[ 'vimspectorPC' ] ) ) + signs.UnplaceSign( self._signs[ 'vimspectorPC' ], 'VimspectorCode' ) self._signs[ 'vimspectorPC' ] = None + self._UndisplayPC() self._UndisplaySigns() + self.current_syntax = None def Reset( self ): self.ClearBreakpoints() @@ -109,25 +175,29 @@ class CodeView( object ): def AddBreakpoints( self, source, breakpoints ): for breakpoint in breakpoints: - if 'source' not in breakpoint: - if source: - breakpoint[ 'source' ] = source - else: - self._logger.warn( 'missing source in breakpoint {0}'.format( - json.dumps( breakpoint ) ) ) - continue + source = breakpoint.get( 'source' ) or source + if not source or 'path' not in source: + self._logger.warn( 'missing source/path in breakpoint {0}'.format( + json.dumps( breakpoint ) ) ) + continue - self._breakpoints[ breakpoint[ 'source' ][ 'path' ] ].append( - breakpoint ) + breakpoint[ 'source' ] = source + self._breakpoints[ source[ 'path' ] ].append( breakpoint ) self._logger.debug( 'Breakpoints at this point: {0}'.format( json.dumps( self._breakpoints, indent = 2 ) ) ) self.ShowBreakpoints() + + def AddBreakpoint( self, breakpoint ): + self.AddBreakpoints( None, [ breakpoint ] ) + + def UpdateBreakpoint( self, bp ): if 'id' not in bp: - self.AddBreakpoints( None, [ bp ] ) + self.AddBreakpoint( bp ) + return for _, breakpoint_list in self._breakpoints.items(): for index, breakpoint in enumerate( breakpoint_list ): @@ -137,11 +207,31 @@ class CodeView( object ): return # Not found. Assume new - self.AddBreakpoints( None, [ bp ] ) + self.AddBreakpoint( bp ) + + + def RemoveBreakpoint( self, bp ): + for _, breakpoint_list in self._breakpoints.items(): + found_index = None + for index, breakpoint in enumerate( breakpoint_list ): + if 'id' in breakpoint and breakpoint[ 'id' ] == bp[ 'id' ]: + found_index = index + break + + if found_index is not None: + del breakpoint_list[ found_index ] + self.ShowBreakpoints() + return + + + def Refresh( self, file_name ): + # TODO: jsut the file ? + self.ShowBreakpoints() + def _UndisplaySigns( self ): for sign_id in self._signs[ 'breakpoints' ]: - vim.command( 'sign unplace {} group=VimspectorCode'.format( sign_id ) ) + signs.UnplaceSign( sign_id, 'VimspectorCode' ) self._signs[ 'breakpoints' ] = [] @@ -160,17 +250,17 @@ class CodeView( object ): sign_id = self._next_sign_id self._next_sign_id += 1 self._signs[ 'breakpoints' ].append( sign_id ) - vim.command( - 'sign place {0} group=VimspectorCode priority=9 ' - 'line={1} ' - 'name={2} ' - 'file={3}'.format( - sign_id, - breakpoint[ 'line' ], - 'vimspectorBP' if breakpoint[ 'verified' ] - else 'vimspectorBPDisabled', - file_name ) ) + if utils.BufferExists( file_name ): + signs.PlaceSign( sign_id, + 'VimspectorCode', + 'vimspectorBP' if breakpoint[ 'verified' ] + else 'vimspectorBPDisabled', + file_name, + breakpoint[ 'line' ] ) + # We need to also check if there's a breakpoint on this PC line and chnge + # the PC + self._DisplayPC() def BreakpointsAsQuickFix( self ): qf = [] @@ -189,50 +279,11 @@ class CodeView( object ): def LaunchTerminal( self, params ): - # kind = params.get( 'kind', 'integrated' ) + self._terminal = terminal.LaunchTerminal( self._api_prefix, + params, + window_for_start = self._window, + existing_term = self._terminal ) - # FIXME: We don't support external terminals, and only open in the - # integrated one. - - cwd = params[ 'cwd' ] - args = params[ 'args' ] - env = params.get( 'env', {} ) - - options = { - 'vertical': 1, - 'norestore': 1, - 'cwd': cwd, - 'env': env, - } - - window_for_start = self._window - if self._terminal_window is not None: - assert self._terminal_buffer_number - if ( self._terminal_window.buffer.number == self._terminal_buffer_number - and 'finished' in vim.eval( 'term_getstatus( {} )'.format( - self._terminal_buffer_number ) ) ): - window_for_start = self._terminal_window - options[ 'curwin' ] = 1 - - buffer_number = None - terminal_window = None - with utils.TemporaryVimOptions( { 'splitright': True, - 'equalalways': False } ): - with utils.LetCurrentWindow( window_for_start ): - # TODO/FIXME: Do something about closing this when we reset ? - vim_cmd = 'term_start( {}, {} )'.format( json.dumps( args ), - json.dumps( options ) ) - - self._logger.debug( 'Start terminal: {}'.format( vim_cmd ) ) - - buffer_number = int( vim.eval( vim_cmd ) ) - terminal_window = vim.current.window - - if buffer_number is None or buffer_number <= 0: - # TODO: Do something better like reject the request? - raise ValueError( "Unable to start terminal" ) - else: - self._terminal_window = terminal_window - self._terminal_buffer_number = buffer_number - - return buffer_number + # FIXME: Change this tor return the PID rather than having debug_session + # work that out + return self._terminal.buffer_number diff --git a/python3/vimspector/custom/java.py b/python3/vimspector/custom/java.py new file mode 100644 index 0000000..c2a2264 --- /dev/null +++ b/python3/vimspector/custom/java.py @@ -0,0 +1,51 @@ +# vimspector - A multi-language debugging system for Vim +# Copyright 2021 Ben Jackson +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from vimspector.debug_session import DebugSession +from vimspector import utils, settings + + +class JavaDebugAdapter( object ): + def __init__( self, debug_session: DebugSession ): + self.debug_session = debug_session + + def OnEvent_hotcodereplace( self, message ): + # Hack for java debug server hot-code-replace + body = message.get( 'body' ) or {} + + if body.get( 'type' ) != 'hotcodereplace': + return + + if body.get( 'changeType' ) == 'BUILD_COMPLETE': + def handler( result ): + if result == 1: + self.debug_session._connection.DoRequest( None, { + 'command': 'redefineClasses', + 'arguments': {}, + } ) + + mode = settings.Get( 'java_hotcodereplace_mode' ) + if mode == 'ask': + utils.Confirm( self.debug_session._api_prefix, + 'Code has changed, hot reload?', + handler, + default_value = 1 ) + elif mode == 'always': + self.debug_session._connection.DoRequest( None, { + 'command': 'redefineClasses', + 'arguments': {}, + } ) + elif body.get( 'message' ): + utils.UserMessage( 'Hot code replace: ' + body[ 'message' ] ) diff --git a/python3/vimspector/debug_adapter_connection.py b/python3/vimspector/debug_adapter_connection.py index 73a4549..206938a 100644 --- a/python3/vimspector/debug_adapter_connection.py +++ b/python3/vimspector/debug_adapter_connection.py @@ -29,14 +29,14 @@ class PendingRequest( object ): class DebugAdapterConnection( object ): - def __init__( self, handler, send_func ): + def __init__( self, handlers, send_func ): self._logger = logging.getLogger( __name__ ) utils.SetUpLogging( self._logger ) self._Write = send_func self._SetState( 'READ_HEADER' ) self._buffer = bytes() - self._handler = handler + self._handlers = handlers self._next_message_id = 0 self._outstanding_requests = {} @@ -65,6 +65,33 @@ class DebugAdapterConnection( object ): if not self._SendMessage( msg ): self._AbortRequest( request, 'Unable to send message' ) + + def DoRequestSync( self, msg, timeout = 5000 ): + result = {} + + def handler( msg ): + result[ 'response' ] = msg + + def failure_handler( reason, msg ): + result[ 'response' ] = msg + result[ 'exception' ] = RuntimeError( reason ) + + self.DoRequest( handler, msg, failure_handler, timeout ) + + bug_catcher = 1000 + while not result and bug_catcher >= 0: + vim.command( 'sleep 10m' ) + bug_catcher -= 10 + + if result.get( 'exception' ) is not None: + raise result[ 'exception' ] + + if result.get( 'response' ) is None: + raise RuntimeError( "No response" ) + + return result[ 'response' ] + + def OnRequestTimeout( self, timer_id ): request_id = None for seq, request in self._outstanding_requests.items(): @@ -97,7 +124,7 @@ class DebugAdapterConnection( object ): def Reset( self ): self._Write = None - self._handler = None + self._handlers = None while self._outstanding_requests: _, request = self._outstanding_requests.popitem() @@ -142,6 +169,10 @@ class DebugAdapterConnection( object ): self._headers = {} def _SendMessage( self, msg ): + if not self._Write: + # Connection was destroyed + return False + msg = json.dumps( msg ) self._logger.debug( 'Sending Message: {0}'.format( msg ) ) @@ -195,7 +226,12 @@ class DebugAdapterConnection( object ): # self._logger.debug( 'Message received (raw): %s', payload ) - message = json.loads( payload ) + try: + message = json.loads( payload, strict = False ) + except Exception: + self._logger.exception( "Invalid message received: %s", payload ) + self._SetState( 'READ_HEADER' ) + raise self._logger.debug( 'Message received: {0}'.format( message ) ) @@ -206,7 +242,7 @@ class DebugAdapterConnection( object ): def _OnMessageReceived( self, message ): - if not self._handler: + if not self._handlers: return if message[ 'type' ] == 'response': @@ -239,25 +275,21 @@ class DebugAdapterConnection( object ): self._logger.error( 'Request failed: {0}'.format( reason ) ) if request.failure_handler: request.failure_handler( reason, message ) - elif 'OnFailure' in dir( self._handler ): - self._handler.OnFailure( reason, message ) else: - utils.UserMessage( 'Request failed: {0}'.format( reason ) ) + for h in self._handlers: + if 'OnFailure' in dir( h ): + h.OnFailure( reason, request.msg, message ) + elif message[ 'type' ] == 'event': method = 'OnEvent_' + message[ 'event' ] - if method in dir( self._handler ): - getattr( self._handler, method )( message ) - else: - utils.UserMessage( 'Unhandled event: {0}'.format( message[ 'event' ] ), - persist = True ) + for h in self._handlers: + if method in dir( h ): + getattr( h, method )( message ) elif message[ 'type' ] == 'request': method = 'OnRequest_' + message[ 'command' ] - if method in dir( self._handler ): - getattr( self._handler, method )( message ) - else: - utils.UserMessage( - 'Unhandled request: {0}'.format( message[ 'command' ] ), - persist = True ) + for h in self._handlers: + if method in dir( h ): + getattr( h, method )( message ) def _KillTimer( request ): diff --git a/python3/vimspector/debug_session.py b/python3/vimspector/debug_session.py index 8bcb3a5..36ad62b 100644 --- a/python3/vimspector/debug_session.py +++ b/python3/vimspector/debug_session.py @@ -13,13 +13,15 @@ # See the License for the specific language governing permissions and # limitations under the License. -import logging -import vim +import glob import json +import logging import os -import subprocess import shlex -import traceback +import subprocess +import functools +import vim +import importlib from vimspector import ( breakpoints, code, @@ -28,95 +30,174 @@ from vimspector import ( breakpoints, output, stack_trace, utils, - variables ) + variables, + settings, + terminal, + installer ) +from vimspector.vendor.json_minify import minify -VIMSPECTOR_HOME = os.path.abspath( os.path.join( os.path.dirname( __file__ ), - '..', - '..' ) ) +# We cache this once, and don't allow it to change (FIXME?) +VIMSPECTOR_HOME = utils.GetVimspectorBase() # cache of what the user entered for any option we ask them USER_CHOICES = {} class DebugSession( object ): - def __init__( self ): + def __init__( self, api_prefix ): self._logger = logging.getLogger( __name__ ) utils.SetUpLogging( self._logger ) + self._api_prefix = api_prefix + self._logger.info( "**** INITIALISING NEW VIMSPECTOR SESSION ****" ) + self._logger.info( "API is: {}".format( api_prefix ) ) self._logger.info( 'VIMSPECTOR_HOME = %s', VIMSPECTOR_HOME ) self._logger.info( 'gadgetDir = %s', - install.GetGadgetDir( VIMSPECTOR_HOME, - install.GetOS() ) ) + install.GetGadgetDir( VIMSPECTOR_HOME ) ) self._uiTab = None + self._logView = None self._stackTraceView = None self._variablesView = None self._outputView = None self._breakpoints = breakpoints.ProjectBreakpoints() + self._splash_screen = None + self._remote_term = None self._run_on_server_exit = None + self._configuration = None + self._adapter = None + self._launch_config = None + self._ResetServerState() def _ResetServerState( self ): self._connection = None - self._configuration = None self._init_complete = False self._launch_complete = False self._on_init_complete_handlers = [] self._server_capabilities = {} + self.ClearTemporaryBreakpoints() + + def GetConfigurations( self, adapters ): + current_file = utils.GetBufferFilepath( vim.current.buffer ) + filetypes = utils.GetBufferFiletypes( vim.current.buffer ) + configurations = {} + + for launch_config_file in PathsToAllConfigFiles( VIMSPECTOR_HOME, + current_file, + filetypes ): + self._logger.debug( f'Reading configurations from: {launch_config_file}' ) + if not launch_config_file or not os.path.exists( launch_config_file ): + continue + + with open( launch_config_file, 'r' ) as f: + database = json.loads( minify( f.read() ) ) + configurations.update( database.get( 'configurations' ) or {} ) + adapters.update( database.get( 'adapters' ) or {} ) + + return launch_config_file, configurations + + def Start( self, force_choose=False, launch_variables = None ): + # We mutate launch_variables, so don't mutate the default argument. + # https://docs.python-guide.org/writing/gotchas/#mutable-default-arguments + if launch_variables is None: + launch_variables = {} - def Start( self, launch_variables = {} ): self._logger.info( "User requested start debug session with %s", launch_variables ) self._configuration = None self._adapter = None + self._launch_config = None - launch_config_file = utils.PathToConfigFile( '.vimspector.json' ) + current_file = utils.GetBufferFilepath( vim.current.buffer ) + adapters = {} + launch_config_file, configurations = self.GetConfigurations( adapters ) - if not launch_config_file: - utils.UserMessage( 'Unable to find .vimspector.json. You need to tell ' - 'vimspector how to launch your application' ) + if not configurations: + utils.UserMessage( 'Unable to find any debug configurations. ' + 'You need to tell vimspector how to launch your ' + 'application.' ) return - with open( launch_config_file, 'r' ) as f: - database = json.load( f ) + glob.glob( install.GetGadgetDir( VIMSPECTOR_HOME ) ) + for gadget_config_file in PathsToAllGadgetConfigs( VIMSPECTOR_HOME, + current_file ): + self._logger.debug( f'Reading gadget config: {gadget_config_file}' ) + if not gadget_config_file or not os.path.exists( gadget_config_file ): + continue - configurations = database.get( 'configurations' ) - adapters = {} - - for gadget_config_file in [ install.GetGadgetConfigFile( VIMSPECTOR_HOME ), - utils.PathToConfigFile( '.gadgets.json' ) ]: - if gadget_config_file and os.path.exists( gadget_config_file ): - with open( gadget_config_file, 'r' ) as f: - adapters.update( json.load( f ).get( 'adapters' ) or {} ) - - adapters.update( database.get( 'adapters' ) or {} ) + with open( gadget_config_file, 'r' ) as f: + a = json.loads( minify( f.read() ) ).get( 'adapters' ) or {} + adapters.update( a ) if 'configuration' in launch_variables: configuration_name = launch_variables.pop( 'configuration' ) - elif len( configurations ) == 1: - configuration_name = next( iter( configurations.keys() ) ) - else: + elif force_choose: + # Always display the menu configuration_name = utils.SelectFromList( 'Which launch configuration?', - sorted( list( configurations.keys() ) ) ) + sorted( configurations.keys() ) ) + elif ( len( configurations ) == 1 and + next( iter( configurations.values() ) ).get( "autoselect", True ) ): + configuration_name = next( iter( configurations.keys() ) ) + else: + # Find a single configuration with 'default' True and autoselect not False + defaults = { n: c for n, c in configurations.items() + if c.get( 'default', False ) is True + and c.get( 'autoselect', True ) is not False } + + if len( defaults ) == 1: + configuration_name = next( iter( defaults.keys() ) ) + else: + configuration_name = utils.SelectFromList( + 'Which launch configuration?', + sorted( configurations.keys() ) ) if not configuration_name or configuration_name not in configurations: return - self._workspace_root = os.path.dirname( launch_config_file ) + if launch_config_file: + self._workspace_root = os.path.dirname( launch_config_file ) + else: + self._workspace_root = os.path.dirname( current_file ) configuration = configurations[ configuration_name ] adapter = configuration.get( 'adapter' ) if isinstance( adapter, str ): - adapter = adapters.get( adapter ) + adapter_dict = adapters.get( adapter ) + + if adapter_dict is None: + suggested_gadgets = installer.FindGadgetForAdapter( adapter ) + if suggested_gadgets: + response = utils.AskForInput( + f"The specified adapter '{adapter}' is not " + "installed. Would you like to install the following gadgets? ", + ' '.join( suggested_gadgets ) ) + if response: + new_launch_variables = dict( launch_variables ) + new_launch_variables[ 'configuration' ] = configuration_name + + installer.RunInstaller( + self._api_prefix, + False, # Don't leave open + *shlex.split( response ), + then = lambda: self.Start( new_launch_variables ) ) + return + elif response is None: + return + + utils.UserMessage( f"The specified adapter '{adapter}' is not " + "available. Did you forget to run " + "'install_gadget.py'?", + persist = True, + error = True ) + return + + adapter = adapter_dict - # TODO: Do we want some form of persistence ? e.g. self._staticVariables, - # set from an api call like SetLaunchParam( 'var', 'value' ), perhaps also a - # way to load .vimspector.local.json which just sets variables - # # Additional vars as defined by VSCode: # # ${workspaceFolder} - the path of the folder opened in VS Code @@ -134,8 +215,6 @@ class DebugSession( object ): # ${selectedText} - the current selected text in the active file # ${execPath} - the path to the running VS Code executable - current_file = utils.GetBufferFilepath( vim.current.buffer ) - def relpath( p, relative_to ): if not p: return '' @@ -146,42 +225,57 @@ class DebugSession( object ): return [ '', '' ] return os.path.splitext( p ) - self._variables = { + variables = { 'dollar': '$', # HACK. Hote '$$' also works. 'workspaceRoot': self._workspace_root, 'workspaceFolder': self._workspace_root, - 'gadgetDir': install.GetGadgetDir( VIMSPECTOR_HOME, install.GetOS() ), + 'gadgetDir': install.GetGadgetDir( VIMSPECTOR_HOME ), 'file': current_file, - 'relativeFile': relpath( current_file, self._workspace_root ), - 'fileBasename': os.path.basename( current_file ), - 'fileBasenameNoExtension': - splitext( os.path.basename( current_file ) )[ 0 ], - 'fileDirname': os.path.dirname( current_file ), - 'fileExtname': splitext( os.path.basename( current_file ) )[ 1 ], - 'cwd': os.getcwd(), } - self._variables.update( - utils.ParseVariables( adapter.get( 'variables', {} ), - self._variables, - USER_CHOICES ) ) - self._variables.update( - utils.ParseVariables( configuration.get( 'variables', {} ), - self._variables, - USER_CHOICES ) ) + + calculus = { + 'relativeFile': lambda: relpath( current_file, + self._workspace_root ), + 'fileBasename': lambda: os.path.basename( current_file ), + 'fileBasenameNoExtension': + lambda: splitext( os.path.basename( current_file ) )[ 0 ], + 'fileDirname': lambda: os.path.dirname( current_file ), + 'fileExtname': lambda: splitext( os.path.basename( current_file ) )[ 1 ], + # NOTE: this is the window-local cwd for the current window, *not* Vim's + # working directory. + 'cwd': os.getcwd, + 'unusedLocalPort': utils.GetUnusedLocalPort, + } # Pretend that vars passed to the launch command were typed in by the user # (they may have been in theory) - # TODO: Is it right that we do this _after_ ParseVariables, rather than - # before ? USER_CHOICES.update( launch_variables ) - self._variables.update( launch_variables ) + variables.update( launch_variables ) - utils.ExpandReferencesInDict( configuration, - self._variables, - USER_CHOICES ) - utils.ExpandReferencesInDict( adapter, - self._variables, - USER_CHOICES ) + try: + variables.update( + utils.ParseVariables( adapter.get( 'variables', {} ), + variables, + calculus, + USER_CHOICES ) ) + variables.update( + utils.ParseVariables( configuration.get( 'variables', {} ), + variables, + calculus, + USER_CHOICES ) ) + + + utils.ExpandReferencesInDict( configuration, + variables, + calculus, + USER_CHOICES ) + utils.ExpandReferencesInDict( adapter, + variables, + calculus, + USER_CHOICES ) + except KeyboardInterrupt: + self._Reset() + return if not adapter: utils.UserMessage( 'No adapter configured for {}'.format( @@ -192,11 +286,9 @@ class DebugSession( object ): def _StartWithConfiguration( self, configuration, adapter ): def start(): - self._logger.debug( "Starting debugger from stack context: %s", - traceback.format_stack() ) - self._configuration = configuration self._adapter = adapter + self._launch_config = None self._logger.info( 'Configuration: %s', json.dumps( self._configuration ) ) @@ -208,6 +300,7 @@ class DebugSession( object ): else: vim.current.tabpage = self._uiTab + self._Prepare() self._StartDebugAdapter() self._Initialise() @@ -233,71 +326,111 @@ class DebugSession( object ): if self._connection: self._logger.debug( "_StopDebugAdapter with callback: start" ) - self._StopDebugAdapter( start ) + self._StopDebugAdapter( interactive = False, callback = start ) return start() def Restart( self ): - # TODO: There is a restart message but isn't always supported. - # FIXME: For some reason this doesn't work when run from the WinBar. It just - # beeps and doesn't display the config selector. One option is to just not - # display the selector and restart with the same opitons. - if not self._configuration or not self._adapter: + if self._configuration is None or self._adapter is None: return self.Start() self._StartWithConfiguration( self._configuration, self._adapter ) + def IfConnected( otherwise=None ): + def decorator( fct ): + """Decorator, call fct if self._connected else echo warning""" + @functools.wraps( fct ) + def wrapper( self, *args, **kwargs ): + if not self._connection: + utils.UserMessage( + 'Vimspector not connected, start a debug session first', + persist=False, + error=True ) + return otherwise + return fct( self, *args, **kwargs ) + return wrapper + return decorator + + def _HasUI( self ): + return self._uiTab and self._uiTab.valid + + def RequiresUI( otherwise=None ): + """Decorator, call fct if self._connected else echo warning""" + def decorator( fct ): + @functools.wraps( fct ) + def wrapper( self, *args, **kwargs ): + if not self._HasUI(): + utils.UserMessage( + 'Vimspector is not active', + persist=False, + error=True ) + return otherwise + return fct( self, *args, **kwargs ) + return wrapper + return decorator + def OnChannelData( self, data ): - if self._connection: - self._connection.OnData( data ) + if self._connection is None: + # Should _not_ happen, but maybe possible due to races or vim bufs? + return + + self._connection.OnData( data ) def OnServerStderr( self, data ): - self._logger.info( "Server stderr: %s", data ) if self._outputView: self._outputView.Print( 'server', data ) def OnRequestTimeout( self, timer_id ): - if self._connection: - self._connection.OnRequestTimeout( timer_id ) + self._connection.OnRequestTimeout( timer_id ) def OnChannelClosed( self ): # TODO: Not calld self._connection = None - def Stop( self ): + @IfConnected() + def Stop( self, interactive = False ): self._logger.debug( "Stop debug adapter with no callback" ) - self._StopDebugAdapter() + self._StopDebugAdapter( interactive = interactive ) - def Reset( self ): + def Reset( self, interactive = False ): if self._connection: self._logger.debug( "Stop debug adapter with callback : self._Reset()" ) - self._StopDebugAdapter( lambda: self._Reset() ) + self._StopDebugAdapter( interactive = interactive, + callback = lambda: self._Reset() ) else: self._Reset() def _Reset( self ): self._logger.info( "Debugging complete." ) if self._uiTab: - self._logger.debug( "Clearing down UI with stack_trace: %s", - traceback.format_stack() ) + self._logger.debug( "Clearing down UI" ) + + del vim.vars[ 'vimspector_session_windows' ] vim.current.tabpage = self._uiTab + + self._splash_screen = utils.HideSplash( self._api_prefix, + self._splash_screen ) + self._stackTraceView.Reset() self._variablesView.Reset() self._outputView.Reset() self._codeView.Reset() vim.command( 'tabclose!' ) + vim.command( 'doautocmd User VimspectorDebugEnded' ) self._stackTraceView = None self._variablesView = None self._outputView = None self._codeView = None + self._remote_term = None self._uiTab = None # make sure that we're displaying signs in any still-open buffers self._breakpoints.UpdateUI() + @IfConnected() def StepOver( self ): if self._stackTraceView.GetCurrentThreadId() is None: return @@ -309,129 +442,418 @@ class DebugSession( object ): }, } ) + self._stackTraceView.OnContinued() + self._codeView.SetCurrentFrame( None ) + + @IfConnected() def StepInto( self ): - if self._stackTraceView.GetCurrentThreadId() is None: + threadId = self._stackTraceView.GetCurrentThreadId() + if threadId is None: return - self._connection.DoRequest( None, { + def handler( *_ ): + self._stackTraceView.OnContinued( { 'threadId': threadId } ) + self._codeView.SetCurrentFrame( None ) + + self._connection.DoRequest( handler, { 'command': 'stepIn', 'arguments': { - 'threadId': self._stackTraceView.GetCurrentThreadId() + 'threadId': threadId }, } ) + @IfConnected() def StepOut( self ): + threadId = self._stackTraceView.GetCurrentThreadId() + if threadId is None: + return + + def handler( *_ ): + self._stackTraceView.OnContinued( { 'threadId': threadId } ) + self._codeView.SetCurrentFrame( None ) + + self._connection.DoRequest( handler, { + 'command': 'stepOut', + 'arguments': { + 'threadId': threadId + }, + } ) + + + def Continue( self ): + if not self._connection: + self.Start() + return + + threadId = self._stackTraceView.GetCurrentThreadId() + if threadId is None: + utils.UserMessage( 'No current thread', persist = True ) + return + + def handler( msg ): + self._stackTraceView.OnContinued( { + 'threadId': threadId, + 'allThreadsContinued': ( msg.get( 'body' ) or {} ).get( + 'allThreadsContinued', + True ) + } ) + self._codeView.SetCurrentFrame( None ) + + self._connection.DoRequest( handler, { + 'command': 'continue', + 'arguments': { + 'threadId': threadId, + }, + } ) + + @IfConnected() + def Pause( self ): if self._stackTraceView.GetCurrentThreadId() is None: + utils.UserMessage( 'No current thread', persist = True ) return self._connection.DoRequest( None, { - 'command': 'stepOut', + 'command': 'pause', 'arguments': { - 'threadId': self._stackTraceView.GetCurrentThreadId() + 'threadId': self._stackTraceView.GetCurrentThreadId(), }, } ) - def Continue( self ): - if self._connection: - self._stackTraceView.Continue() - else: - self.Start() + @IfConnected() + def PauseContinueThread( self ): + self._stackTraceView.PauseContinueThread() - def Pause( self ): - self._stackTraceView.Pause() + @IfConnected() + def SetCurrentThread( self ): + self._stackTraceView.SetCurrentThread() - def ExpandVariable( self ): - self._variablesView.ExpandVariable() + @IfConnected() + def ExpandVariable( self, buf = None, line_num = None ): + self._variablesView.ExpandVariable( buf, line_num ) + @IfConnected() + def SetVariableValue( self, new_value = None, buf = None, line_num = None ): + self._variablesView.SetVariableValue( new_value, buf, line_num ) + + @IfConnected() def AddWatch( self, expression ): self._variablesView.AddWatch( self._stackTraceView.GetCurrentFrame(), expression ) - def EvaluateConsole( self, expression ): + @IfConnected() + def EvaluateConsole( self, expression, verbose ): self._outputView.Evaluate( self._stackTraceView.GetCurrentFrame(), - expression ) + expression, + verbose ) + @IfConnected() def DeleteWatch( self ): self._variablesView.DeleteWatch() - def ShowBalloon( self, winnr, expression ): - if self._stackTraceView.GetCurrentFrame() is None: - return - if winnr == int( self._codeView._window.number ): - self._variablesView.ShowBalloon( self._stackTraceView.GetCurrentFrame(), - expression ) - else: + @IfConnected() + def ShowEvalBalloon( self, winnr, expression, is_hover ): + frame = self._stackTraceView.GetCurrentFrame() + # Check if RIP is in a frame + if frame is None: + self._logger.debug( 'Tooltip: Not in a stack frame' ) + return '' + + # Check if cursor in code window + if winnr != int( self._codeView._window.number ): self._logger.debug( 'Winnr %s is not the code window %s', winnr, self._codeView._window.number ) + return '' + # Return variable aware function + return self._variablesView.VariableEval( frame, expression, is_hover ) + + + def CleanUpTooltip( self ): + return self._variablesView.CleanUpTooltip() + + @IfConnected() def ExpandFrameOrThread( self ): self._stackTraceView.ExpandFrameOrThread() + @IfConnected() + def UpFrame( self ): + self._stackTraceView.UpFrame() + + @IfConnected() + def DownFrame( self ): + self._stackTraceView.DownFrame() + + def ToggleLog( self ): + if self._HasUI(): + return self.ShowOutput( 'Vimspector' ) + + if self._logView and self._logView.WindowIsValid(): + self._logView.Reset() + self._logView = None + return + + if self._logView: + self._logView.Reset() + + # TODO: The UI code is too scattered. Re-organise into a UI class that + # just deals with these thigns like window layout and custmisattion. + vim.command( f'botright { settings.Int( "bottombar_height" ) }new' ) + win = vim.current.window + self._logView = output.OutputView( win, self._api_prefix ) + self._logView.AddLogFileView() + self._logView.ShowOutput( 'Vimspector' ) + + @RequiresUI() def ShowOutput( self, category ): + if not self._outputView.WindowIsValid(): + # TODO: The UI code is too scattered. Re-organise into a UI class that + # just deals with these thigns like window layout and custmisattion. + # currently, this class and the CodeView share some responsiblity for this + # and poking into each View class to check its window is valid also feels + # wrong. + with utils.LetCurrentTabpage( self._uiTab ): + vim.command( f'botright { settings.Int( "bottombar_height" ) }new' ) + self._outputView.UseWindow( vim.current.window ) + vim.vars[ 'vimspector_session_windows' ][ 'output' ] = utils.WindowID( + vim.current.window, + self._uiTab ) + self._outputView.ShowOutput( category ) + @RequiresUI( otherwise=[] ) + def GetOutputBuffers( self ): + return self._outputView.GetCategories() + + @IfConnected( otherwise=[] ) + def GetCompletionsSync( self, text_line, column_in_bytes ): + if not self._server_capabilities.get( 'supportsCompletionsRequest' ): + return [] + + response = self._connection.DoRequestSync( { + 'command': 'completions', + 'arguments': { + 'frameId': self._stackTraceView.GetCurrentFrame()[ 'id' ], + # TODO: encoding ? bytes/codepoints + 'text': text_line, + 'column': column_in_bytes + } + } ) + # TODO: + # - start / length + # - sortText + return response[ 'body' ][ 'targets' ] + + + @IfConnected( otherwise=[] ) + def GetCommandLineCompletions( self, ArgLead, prev_non_keyword_char ): + items = [] + for candidate in self.GetCompletionsSync( ArgLead, prev_non_keyword_char ): + label = candidate.get( 'text', candidate[ 'label' ] ) + start = prev_non_keyword_char - 1 + if 'start' in candidate and 'length' in candidate: + start = candidate[ 'start' ] + items.append( ArgLead[ 0 : start ] + label ) + + return items + + def RefreshSigns( self, file_name ): + if self._connection: + self._codeView.Refresh( file_name ) + else: + self._breakpoints.Refresh( file_name ) + + def _SetUpUI( self ): - vim.command( 'tabnew' ) + vim.command( 'tab split' ) self._uiTab = vim.current.tabpage + mode = settings.Get( 'ui_mode' ) + + if mode == 'auto': + # Go vertical if there isn't enough horizontal space for at least: + # the left bar width + # + the code min width + # + the terminal min width + # + enough space for a sign column and number column? + min_width = ( settings.Int( 'sidebar_width' ) + + 1 + 2 + 3 + + settings.Int( 'code_minwidth' ) + + 1 + settings.Int( 'terminal_minwidth' ) ) + + min_height = ( settings.Int( 'code_minheight' ) + 1 + + settings.Int( 'topbar_height' ) + 1 + + settings.Int( 'bottombar_height' ) + 1 + + 2 ) + + mode = ( 'vertical' + if vim.options[ 'columns' ] < min_width + else 'horizontal' ) + + if vim.options[ 'lines' ] < min_height: + mode = 'horizontal' + + self._logger.debug( 'min_width/height: %s/%s, actual: %s/%s - result: %s', + min_width, + min_height, + vim.options[ 'columns' ], + vim.options[ 'lines' ], + mode ) + + if mode == 'vertical': + self._SetUpUIVertical() + else: + self._SetUpUIHorizontal() + + + def _SetUpUIHorizontal( self ): # Code window - self._codeView = code.CodeView( vim.current.window ) + code_window = vim.current.window + self._codeView = code.CodeView( code_window, self._api_prefix ) # Call stack - with utils.TemporaryVimOptions( { 'splitright': False, - 'equalalways': False, } ): - vim.command( 'topleft 50vspl' ) - vim.command( 'enew' ) - self._stackTraceView = stack_trace.StackTraceView( self, - self._connection, - vim.current.buffer ) + vim.command( + f'topleft vertical { settings.Int( "sidebar_width" ) }new' ) + stack_trace_window = vim.current.window + one_third = int( vim.eval( 'winheight( 0 )' ) ) / 3 + self._stackTraceView = stack_trace.StackTraceView( self, + stack_trace_window ) - with utils.TemporaryVimOptions( { 'splitbelow': False, - 'eadirection': 'ver', - 'equalalways': True } ): - # Watches - vim.command( 'spl' ) - vim.command( 'enew' ) - watch_win = vim.current.window + # Watches + vim.command( 'leftabove new' ) + watch_window = vim.current.window - # Variables - vim.command( 'spl' ) - vim.command( 'enew' ) - vars_win = vim.current.window + # Variables + vim.command( 'leftabove new' ) + vars_window = vim.current.window - self._variablesView = variables.VariablesView( self._connection, - vars_win, - watch_win ) + with utils.LetCurrentWindow( vars_window ): + vim.command( f'{ one_third }wincmd _' ) + with utils.LetCurrentWindow( watch_window ): + vim.command( f'{ one_third }wincmd _' ) + with utils.LetCurrentWindow( stack_trace_window ): + vim.command( f'{ one_third }wincmd _' ) + + self._variablesView = variables.VariablesView( vars_window, + watch_window ) + + # Output/logging + vim.current.window = code_window + vim.command( f'rightbelow { settings.Int( "bottombar_height" ) }new' ) + output_window = vim.current.window + self._outputView = output.DAPOutputView( output_window, + self._api_prefix ) + + # TODO: If/when we support multiple sessions, we'll need some way to + # indicate which tab was created and store all the tabs + vim.vars[ 'vimspector_session_windows' ] = { + 'mode': 'horizontal', + 'tabpage': self._uiTab.number, + 'code': utils.WindowID( code_window, self._uiTab ), + 'stack_trace': utils.WindowID( stack_trace_window, self._uiTab ), + 'variables': utils.WindowID( vars_window, self._uiTab ), + 'watches': utils.WindowID( watch_window, self._uiTab ), + 'output': utils.WindowID( output_window, self._uiTab ), + 'eval': None # this is going to be updated every time eval popup is opened + } + with utils.RestoreCursorPosition(): + with utils.RestoreCurrentWindow(): + with utils.RestoreCurrentBuffer( vim.current.window ): + vim.command( 'doautocmd User VimspectorUICreated' ) - with utils.TemporaryVimOption( 'splitbelow', True ): - vim.current.window = self._codeView._window + def _SetUpUIVertical( self ): + # Code window + code_window = vim.current.window + self._codeView = code.CodeView( code_window, self._api_prefix ) - # Output/logging - vim.command( '10spl' ) - vim.command( 'enew' ) - self._outputView = output.OutputView( self._connection, - vim.current.window ) + # Call stack + vim.command( + f'topleft { settings.Int( "topbar_height" ) }new' ) + stack_trace_window = vim.current.window + one_third = int( vim.eval( 'winwidth( 0 )' ) ) / 3 + self._stackTraceView = stack_trace.StackTraceView( self, + stack_trace_window ) + + # Watches + vim.command( 'leftabove vertical new' ) + watch_window = vim.current.window + + # Variables + vim.command( 'leftabove vertical new' ) + vars_window = vim.current.window + + + with utils.LetCurrentWindow( vars_window ): + vim.command( f'{ one_third }wincmd |' ) + with utils.LetCurrentWindow( watch_window ): + vim.command( f'{ one_third }wincmd |' ) + with utils.LetCurrentWindow( stack_trace_window ): + vim.command( f'{ one_third }wincmd |' ) + + self._variablesView = variables.VariablesView( vars_window, + watch_window ) + + + # Output/logging + vim.current.window = code_window + vim.command( f'rightbelow { settings.Int( "bottombar_height" ) }new' ) + output_window = vim.current.window + self._outputView = output.DAPOutputView( output_window, + self._api_prefix ) + + # TODO: If/when we support multiple sessions, we'll need some way to + # indicate which tab was created and store all the tabs + vim.vars[ 'vimspector_session_windows' ] = { + 'mode': 'vertical', + 'tabpage': self._uiTab.number, + 'code': utils.WindowID( code_window, self._uiTab ), + 'stack_trace': utils.WindowID( stack_trace_window, self._uiTab ), + 'variables': utils.WindowID( vars_window, self._uiTab ), + 'watches': utils.WindowID( watch_window, self._uiTab ), + 'output': utils.WindowID( output_window, self._uiTab ), + 'eval': None # this is going to be updated every time eval popup is opened + } + with utils.RestoreCursorPosition(): + with utils.RestoreCurrentWindow(): + with utils.RestoreCurrentBuffer( vim.current.window ): + vim.command( 'doautocmd User VimspectorUICreated' ) + + + @RequiresUI() def ClearCurrentFrame( self ): self.SetCurrentFrame( None ) - def SetCurrentFrame( self, frame ): + @RequiresUI() + def SetCurrentFrame( self, frame, reason = '' ): + if not frame: + self._stackTraceView.Clear() + self._variablesView.Clear() + if not self._codeView.SetCurrentFrame( frame ): return False - if frame: - self._variablesView.LoadScopes( frame ) - self._variablesView.EvaluateWatches() - else: - self._stackTraceView.Clear() - self._variablesView.Clear() + # the codeView.SetCurrentFrame already checked the frame was valid and + # countained a valid source + self._variablesView.SetSyntax( self._codeView.current_syntax ) + self._stackTraceView.SetSyntax( self._codeView.current_syntax ) + self._variablesView.LoadScopes( frame ) + self._variablesView.EvaluateWatches() + + if reason == 'stopped': + self._breakpoints.ClearTemporaryBreakpoint( frame[ 'source' ][ 'path' ], + frame[ 'line' ] ) return True def _StartDebugAdapter( self ): + self._splash_screen = utils.DisplaySplash( + self._api_prefix, + self._splash_screen, + "Starting debug adapter..." ) + if self._connection: utils.UserMessage( 'The connection is already created. Please try again', persist = True ) @@ -441,7 +863,6 @@ class DebugSession( object ): json.dumps( self._adapter ) ) self._init_complete = False - self._on_init_complete_handlers = [] self._launch_complete = False self._run_on_server_exit = None @@ -451,75 +872,122 @@ class DebugSession( object ): if self._adapter[ 'port' ] == 'ask': port = utils.AskForInput( 'Enter port to connect to: ' ) + if port is None: + self._Reset() + return self._adapter[ 'port' ] = port - # TODO: Do we actually need to copy and update or does Vim do that? - env = os.environ.copy() - if 'env' in self._adapter: - env.update( self._adapter[ 'env' ] ) - self._adapter[ 'env' ] = env + self._connection_type = self._api_prefix + self._connection_type + self._logger.debug( f"Connection Type: { self._connection_type }" ) + + self._adapter[ 'env' ] = self._adapter.get( 'env', {} ) if 'cwd' not in self._adapter: self._adapter[ 'cwd' ] = os.getcwd() - channel_send_func = vim.bindeval( - "vimspector#internal#{}#StartDebugSession( {} )".format( - self._connection_type, - json.dumps( self._adapter ) ) ) - - if channel_send_func is None: + vim.vars[ '_vimspector_adapter_spec' ] = self._adapter + if not vim.eval( "vimspector#internal#{}#StartDebugSession( " + " g:_vimspector_adapter_spec " + ")".format( self._connection_type ) ): self._logger.error( "Unable to start debug server" ) + self._splash_screen = utils.DisplaySplash( self._api_prefix, + self._splash_screen, + "Unable to start adapter" ) else: + if 'custom_handler' in self._adapter: + spec = self._adapter[ 'custom_handler' ] + if isinstance( spec, dict ): + module = spec[ 'module' ] + cls = spec[ 'class' ] + else: + module, cls = spec.rsplit( '.', 1 ) + + CustomHandler = getattr( importlib.import_module( module ), cls ) + handlers = [ CustomHandler( self ), self ] + else: + handlers = [ self ] + self._connection = debug_adapter_connection.DebugAdapterConnection( - self, - channel_send_func ) + handlers, + lambda msg: utils.Call( + "vimspector#internal#{}#Send".format( self._connection_type ), + msg ) ) - self._logger.info( 'Debug Adapter Started' ) - - def _StopDebugAdapter( self, callback = None ): - def handler( *args ): - if callback: - self._logger.debug( "Setting server exit handler before disconnect" ) - assert not self._run_on_server_exit - self._run_on_server_exit = callback - - vim.eval( 'vimspector#internal#{}#StopDebugSession()'.format( - self._connection_type ) ) + self._logger.info( 'Debug Adapter Started' ) + def _StopDebugAdapter( self, interactive = False, callback = None ): arguments = {} - if self._server_capabilities.get( 'supportTerminateDebuggee' ): - arguments[ 'terminateDebugee' ] = True - self._connection.DoRequest( handler, { - 'command': 'disconnect', - 'arguments': arguments, - }, failure_handler = handler, timeout = 5000 ) + def disconnect(): + self._splash_screen = utils.DisplaySplash( + self._api_prefix, + self._splash_screen, + "Shutting down debug adapter..." ) + + def handler( *args ): + self._splash_screen = utils.HideSplash( self._api_prefix, + self._splash_screen ) + + if callback: + self._logger.debug( "Setting server exit handler before disconnect" ) + assert not self._run_on_server_exit + self._run_on_server_exit = callback + + vim.eval( 'vimspector#internal#{}#StopDebugSession()'.format( + self._connection_type ) ) + + self._connection.DoRequest( handler, { + 'command': 'disconnect', + 'arguments': arguments, + }, failure_handler = handler, timeout = 5000 ) + + if not interactive: + disconnect() + elif not self._server_capabilities.get( 'supportTerminateDebuggee' ): + disconnect() + elif not self._stackTraceView.AnyThreadsRunning(): + disconnect() + else: + def handle_choice( choice ): + if choice == 1: + # yes + arguments[ 'terminateDebuggee' ] = True + elif choice == 2: + # no + arguments[ 'terminateDebuggee' ] = False + elif choice <= 0: + # Abort + return + # Else, use server default + + disconnect() + + utils.Confirm( self._api_prefix, + "Terminate debuggee?", + handle_choice, + default_value = 3, + options = [ '(Y)es', '(N)o', '(D)efault' ], + keys = [ 'y', 'n', 'd' ] ) - # TODO: Use the 'tarminate' request if supportsTerminateRequest set def _PrepareAttach( self, adapter_config, launch_config ): + attach_config = adapter_config.get( 'attach' ) - atttach_config = adapter_config.get( 'attach' ) - - if not atttach_config: + if not attach_config: return - if 'remote' in atttach_config: + if 'remote' in attach_config: # FIXME: We almost want this to feed-back variables to be expanded later, # e.g. expand variables when we use them, not all at once. This would # remove the whole %PID% hack. - remote = atttach_config[ 'remote' ] - ssh = [ 'ssh' ] + remote = attach_config[ 'remote' ] + remote_exec_cmd = self._GetRemoteExecCommand( remote ) - if 'account' in remote: - ssh.append( remote[ 'account' ] + '@' + remote[ 'host' ] ) - else: - ssh.append( remote[ 'host' ] ) + # FIXME: Why does this not use self._GetCommands ? + pid_cmd = remote_exec_cmd + remote[ 'pidCommand' ] - cmd = ssh + remote[ 'pidCommand' ] - - self._logger.debug( 'Getting PID: %s', cmd ) - pid = subprocess.check_output( cmd ).decode( 'utf-8' ).strip() + self._logger.debug( 'Getting PID: %s', pid_cmd ) + pid = subprocess.check_output( pid_cmd ).decode( 'utf-8' ).strip() self._logger.debug( 'Got PID: %s', pid ) if not pid: @@ -528,7 +996,7 @@ class DebugSession( object ): return if 'initCompleteCommand' in remote: - initcmd = ssh + remote[ 'initCompleteCommand' ][ : ] + initcmd = remote_exec_cmd + remote[ 'initCompleteCommand' ][ : ] for index, item in enumerate( initcmd ): initcmd[ index ] = item.replace( '%PID%', pid ) @@ -538,24 +1006,38 @@ class DebugSession( object ): commands = self._GetCommands( remote, 'attach' ) for command in commands: - cmd = ssh + command[ : ] + cmd = remote_exec_cmd + command for index, item in enumerate( cmd ): cmd[ index ] = item.replace( '%PID%', pid ) self._logger.debug( 'Running remote app: %s', cmd ) - self._outputView.RunJobWithOutput( 'Remote', cmd ) + self._remote_term = terminal.LaunchTerminal( + self._api_prefix, + { + 'args': cmd, + 'cwd': os.getcwd() + }, + self._codeView._window, + self._remote_term ) else: - if atttach_config[ 'pidSelect' ] == 'ask': - pid = utils.AskForInput( 'Enter PID to attach to: ' ) - launch_config[ atttach_config[ 'pidProperty' ] ] = pid + if attach_config[ 'pidSelect' ] == 'ask': + prop = attach_config[ 'pidProperty' ] + if prop not in launch_config: + pid = utils.AskForInput( 'Enter PID to attach to: ' ) + if pid is None: + return + launch_config[ prop ] = pid return - elif atttach_config[ 'pidSelect' ] == 'none': + elif attach_config[ 'pidSelect' ] == 'none': return raise ValueError( 'Unrecognised pidSelect {0}'.format( - atttach_config[ 'pidSelect' ] ) ) + attach_config[ 'pidSelect' ] ) ) + if 'delay' in attach_config: + utils.UserMessage( f"Waiting ( { attach_config[ 'delay' ] } )..." ) + vim.command( f'sleep { attach_config[ "delay" ] }' ) def _PrepareLaunch( self, command_line, adapter_config, launch_config ): @@ -563,16 +1045,11 @@ class DebugSession( object ): if 'remote' in run_config: remote = run_config[ 'remote' ] - ssh = [ 'ssh' ] - if 'account' in remote: - ssh.append( remote[ 'account' ] + '@' + remote[ 'host' ] ) - else: - ssh.append( remote[ 'host' ] ) - + remote_exec_cmd = self._GetRemoteExecCommand( remote ) commands = self._GetCommands( remote, 'run' ) for index, command in enumerate( commands ): - cmd = ssh + command[ : ] + cmd = remote_exec_cmd + command[ : ] full_cmd = [] for item in cmd: if isinstance( command_line, list ): @@ -584,8 +1061,46 @@ class DebugSession( object ): full_cmd.append( item.replace( '%CMD%', command_line ) ) self._logger.debug( 'Running remote app: %s', full_cmd ) - self._outputView.RunJobWithOutput( 'Remote{}'.format( index ), - full_cmd ) + self._remote_term = terminal.LaunchTerminal( + self._api_prefix, + { + 'args': full_cmd, + 'cwd': os.getcwd() + }, + self._codeView._window, + self._remote_term ) + + if 'delay' in run_config: + utils.UserMessage( f"Waiting ( {run_config[ 'delay' ]} )..." ) + vim.command( f'sleep { run_config[ "delay" ] }' ) + + + + def _GetSSHCommand( self, remote ): + ssh = [ 'ssh' ] + remote.get( 'ssh', {} ).get( 'args', [] ) + if 'account' in remote: + ssh.append( remote[ 'account' ] + '@' + remote[ 'host' ] ) + else: + ssh.append( remote[ 'host' ] ) + + return ssh + + def _GetDockerCommand( self, remote ): + docker = [ 'docker', 'exec' ] + docker.append( remote[ 'container' ] ) + return docker + + def _GetRemoteExecCommand( self, remote ): + is_ssh_cmd = any( key in remote for key in [ 'ssh', + 'host', + 'account', ] ) + is_docker_cmd = 'container' in remote + + if is_ssh_cmd: + return self._GetSSHCommand( remote ) + elif is_docker_cmd: + return self._GetDockerCommand( remote ) + raise ValueError( 'Could not determine remote exec command' ) def _GetCommands( self, remote, pfx ): @@ -610,6 +1125,11 @@ class DebugSession( object ): return [ command ] def _Initialise( self ): + self._splash_screen = utils.DisplaySplash( + self._api_prefix, + self._splash_screen, + "Initializing debug adapter..." ) + # For a good explaination as to why this sequence is the way it is, see # https://github.com/microsoft/vscode/issues/4902#issuecomment-368583522 # @@ -628,6 +1148,7 @@ class DebugSession( object ): def handle_initialize_response( msg ): self._server_capabilities = msg.get( 'body' ) or {} self._breakpoints.SetServerCapabilities( self._server_capabilities ) + self._variablesView.SetServerCapabilities( self._server_capabilities ) self._Launch() self._connection.DoRequest( handle_initialize_response, { @@ -647,40 +1168,69 @@ class DebugSession( object ): } ) - def OnFailure( self, reason, message ): - msg = "Request for '{}' failed: {}".format( message[ 'command' ], - reason ) + def OnFailure( self, reason, request, message ): + msg = "Request for '{}' failed: {}\nResponse: {}".format( request, + reason, + message ) self._outputView.Print( 'server', msg ) - def _Launch( self ): + + def _Prepare( self ): + self._on_init_complete_handlers = [] + self._logger.debug( "LAUNCH!" ) - adapter_config = self._adapter - launch_config = self._configuration[ 'configuration' ] + self._launch_config = {} + self._launch_config.update( self._adapter.get( 'configuration', {} ) ) + self._launch_config.update( self._configuration[ 'configuration' ] ) request = self._configuration.get( 'remote-request', - launch_config.get( 'request', 'launch' ) ) + self._launch_config.get( 'request', 'launch' ) ) if request == "attach": - self._PrepareAttach( adapter_config, launch_config ) + self._splash_screen = utils.DisplaySplash( + self._api_prefix, + self._splash_screen, + "Attaching to debuggee..." ) + + self._PrepareAttach( self._adapter, self._launch_config ) elif request == "launch": + self._splash_screen = utils.DisplaySplash( + self._api_prefix, + self._splash_screen, + "Launching debuggee..." ) + # FIXME: This cmdLine hack is not fun. self._PrepareLaunch( self._configuration.get( 'remote-cmdLine', [] ), - adapter_config, - launch_config ) + self._adapter, + self._launch_config ) # FIXME: name is mandatory. Forcefully add it (we should really use the # _actual_ name, but that isn't actually remembered at this point) - if 'name' not in launch_config: - launch_config[ 'name' ] = 'test' + if 'name' not in self._launch_config: + self._launch_config[ 'name' ] = 'test' + + + def _Launch( self ): + def failure_handler( reason, msg ): + text = [ + 'Launch Failed', + '', + reason, + '', + 'Use :VimspectorReset to close' + ] + self._splash_screen = utils.DisplaySplash( self._api_prefix, + self._splash_screen, + text ) self._connection.DoRequest( lambda msg: self._OnLaunchComplete(), { - 'command': launch_config[ 'request' ], - 'arguments': launch_config - } - ) + 'command': self._launch_config[ 'request' ], + 'arguments': self._launch_config + }, + failure_handler ) def _OnLaunchComplete( self ): @@ -709,6 +1259,9 @@ class DebugSession( object ): # leader rather than the process. The workaround is to manually SIGTRAP the # PID. # + self._splash_screen = utils.HideSplash( self._api_prefix, + self._splash_screen ) + if self._launch_complete and self._init_complete: for h in self._on_init_complete_handlers: h() @@ -717,6 +1270,41 @@ class DebugSession( object ): self._stackTraceView.LoadThreads( True ) + @IfConnected() + @RequiresUI() + def PrintDebugInfo( self ): + def Line(): + return ( "--------------------------------------------------------------" + "------------------" ) + + def Pretty( obj ): + if obj is None: + return [ "None" ] + return [ Line() ] + json.dumps( obj, indent=2 ).splitlines() + [ Line() ] + + + debugInfo = [ + "Vimspector Debug Info", + Line(), + f"ConnectionType: { self._connection_type }", + "Adapter: " ] + Pretty( self._adapter ) + [ + "Configuration: " ] + Pretty( self._configuration ) + [ + f"API Prefix: { self._api_prefix }", + f"Launch/Init: { self._launch_complete } / { self._init_complete }", + f"Workspace Root: { self._workspace_root }", + "Launch Config: " ] + Pretty( self._launch_config ) + [ + "Server Capabilities: " ] + Pretty( self._server_capabilities ) + [ + ] + + self._outputView.ClearCategory( 'DebugInfo' ) + self._outputView.Print( "DebugInfo", debugInfo ) + self.ShowOutput( "DebugInfo" ) + + + def OnEvent_loadedSource( self, msg ): + pass + + def OnEvent_capabilities( self, msg ): self._server_capabilities.update( ( msg.get( 'body' ) or {} ).get( 'capabilities' ) or {} ) @@ -735,6 +1323,8 @@ class DebugSession( object ): self._OnInitializeComplete() self._codeView.ClearBreakpoints() + self._breakpoints.SetConfiguredBreakpoints( + self._configuration.get( 'breakpoints', {} ) ) self._breakpoints.SendBreakpoints( onBreakpointsDone ) def OnEvent_thread( self, message ): @@ -747,7 +1337,9 @@ class DebugSession( object ): if reason == 'changed': self._codeView.UpdateBreakpoint( bp ) elif reason == 'new': - self._codeView.AddBreakpoints( None, bp ) + self._codeView.AddBreakpoint( bp ) + elif reason == 'removed': + self._codeView.RemoveBreakpoint( bp ) else: utils.UserMessage( 'Unrecognised breakpoint event (undocumented): {0}'.format( reason ), @@ -761,28 +1353,43 @@ class DebugSession( object ): self._logger.debug( 'Defaulting working directory to %s', params[ 'cwd' ] ) - buffer_number = self._codeView.LaunchTerminal( params ) + term_id = self._codeView.LaunchTerminal( params ) response = { - 'processId': vim.eval( 'job_info( term_getjob( {} ) )' - '.process'.format( buffer_number ) ) + 'processId': int( utils.Call( + 'vimspector#internal#{}term#GetPID'.format( self._api_prefix ), + term_id ) ) } self._connection.DoResponse( message, None, response ) + def OnEvent_terminated( self, message ): + # The debugging _session_ has terminated. This does not mean that the + # debuggee has terminated (that's the exited event). + # + # We will handle this when the server actually exists. + # + # FIXME we should always wait for this event before disconnecting closing + # any socket connection + self.SetCurrentFrame( None ) + + def OnEvent_exited( self, message ): - utils.UserMessage( 'The debugee exited with status code: {}'.format( + utils.UserMessage( 'The debuggee exited with status code: {}'.format( message[ 'body' ][ 'exitCode' ] ) ) + self._stackTraceView.OnExited( message ) + self._codeView.SetCurrentFrame( None ) def OnEvent_process( self, message ): - utils.UserMessage( 'The debugee was started: {}'.format( + utils.UserMessage( 'The debuggee was started: {}'.format( message[ 'body' ][ 'name' ] ) ) def OnEvent_module( self, message ): pass def OnEvent_continued( self, message ): - pass + self._stackTraceView.OnContinued( message[ 'body' ] ) + self._codeView.SetCurrentFrame( None ) def Clear( self ): self._codeView.Clear() @@ -794,7 +1401,11 @@ class DebugSession( object ): status ) self.Clear() - self._connection.Reset() + if self._connection is not None: + # Can be None if the server dies _before_ StartDebugSession vim function + # returns + self._connection.Reset() + self._stackTraceView.ConnectionClosed() self._variablesView.ConnectionClosed() self._outputView.ConnectionClosed() @@ -804,14 +1415,12 @@ class DebugSession( object ): if self._run_on_server_exit: self._logger.debug( "Running server exit handler" ) - self._run_on_server_exit() + callback = self._run_on_server_exit + self._run_on_server_exit = None + callback() else: self._logger.debug( "No server exit handler" ) - def OnEvent_terminated( self, message ): - # We will handle this when the server actually exists - utils.UserMessage( "Debugging was terminated by the server." ) - def OnEvent_output( self, message ): if self._outputView: self._outputView.OnOutput( message[ 'body' ] ) @@ -841,10 +1450,36 @@ class DebugSession( object ): self._stackTraceView.OnStopped( event ) def ListBreakpoints( self ): - return self._breakpoints.ListBreakpoints() + if self._connection: + qf = self._codeView.BreakpointsAsQuickFix() + else: + qf = self._breakpoints.BreakpointsAsQuickFix() - def ToggleBreakpoint( self ): - return self._breakpoints.ToggleBreakpoint() + vim.eval( 'setqflist( {} )'.format( json.dumps( qf ) ) ) + vim.command( 'copen' ) + + def ToggleBreakpoint( self, options ): + return self._breakpoints.ToggleBreakpoint( options ) + + def RunTo( self, file_name, line ): + self.ClearTemporaryBreakpoints() + self.SetLineBreakpoint( file_name, + line, + { 'temporary': True }, + lambda: self.Continue() ) + + + def ClearTemporaryBreakpoints( self ): + return self._breakpoints.ClearTemporaryBreakpoints() + + def SetLineBreakpoint( self, file_name, line_num, options, then = None ): + return self._breakpoints.SetLineBreakpoint( file_name, + line_num, + options, + then ) + + def ClearLineBreakpoint( self, file_name, line_num ): + return self._breakpoints.ClearLineBreakpoint( file_name, line_num ) def ClearBreakpoints( self ): if self._connection: @@ -852,5 +1487,31 @@ class DebugSession( object ): return self._breakpoints.ClearBreakpoints() - def AddFunctionBreakpoint( self, function ): - return self._breakpoints.AddFunctionBreakpoint( function ) + def AddFunctionBreakpoint( self, function, options ): + return self._breakpoints.AddFunctionBreakpoint( function, options ) + + +def PathsToAllGadgetConfigs( vimspector_base, current_file ): + yield install.GetGadgetConfigFile( vimspector_base ) + for p in sorted( glob.glob( + os.path.join( install.GetGadgetConfigDir( vimspector_base ), + '*.json' ) ) ): + yield p + + yield utils.PathToConfigFile( '.gadgets.json', + os.path.dirname( current_file ) ) + + +def PathsToAllConfigFiles( vimspector_base, current_file, filetypes ): + for ft in filetypes + [ '_all' ]: + for p in sorted( glob.glob( + os.path.join( install.GetConfigDirForFiletype( vimspector_base, ft ), + '*.json' ) ) ): + yield p + + for ft in filetypes: + yield utils.PathToConfigFile( f'.vimspector.{ft}.json', + os.path.dirname( current_file ) ) + + yield utils.PathToConfigFile( '.vimspector.json', + os.path.dirname( current_file ) ) diff --git a/python3/vimspector/developer.py b/python3/vimspector/developer.py new file mode 100644 index 0000000..42de1af --- /dev/null +++ b/python3/vimspector/developer.py @@ -0,0 +1,41 @@ +# vimspector - A multi-language debugging system for Vim +# Copyright 2020 Ben Jackson +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +import sys +import os + +from vimspector import install, utils, installer + + +def SetUpDebugpy( wait=False, port=5678 ): + sys.path.insert( + 1, + os.path.join( install.GetGadgetDir( utils.GetVimspectorBase() ), + 'debugpy', + 'build', + 'lib' ) ) + import debugpy + + exe = sys.executable + try: + # debugpy uses sys.executable (which is `vim`, so we hack it) + sys.executable = installer.PathToAnyWorkingPython3() + debugpy.listen( port ) + finally: + sys.executable = exe + + if wait: + debugpy.wait_for_client() diff --git a/python3/vimspector/gadgets.py b/python3/vimspector/gadgets.py new file mode 100644 index 0000000..02eb0e7 --- /dev/null +++ b/python3/vimspector/gadgets.py @@ -0,0 +1,475 @@ +# vimspector - A multi-language debugging system for Vim +# Copyright 2020 Ben Jackson +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +from vimspector import installer +import sys +import os + + +GADGETS = { + 'vscode-cpptools': { + 'language': [ 'c', 'cpp', 'rust' ], + 'download': { + 'url': 'https://github.com/Microsoft/vscode-cpptools/releases/download/' + '${version}/${file_name}', + }, + 'do': lambda name, root, gadget: installer.InstallCppTools( name, + root, + gadget ), + 'all': { + 'version': '1.6.0', + "adapters": { + "vscode-cpptools": { + "name": "cppdbg", + "command": [ + "${gadgetDir}/vscode-cpptools/debugAdapters/bin/OpenDebugAD7" + ], + "attach": { + "pidProperty": "processId", + "pidSelect": "ask" + }, + "configuration": { + "type": "cppdbg", + "args": [], + "cwd": "${workspaceRoot}", + "environment": [], + } + }, + }, + }, + 'linux': { + 'file_name': 'cpptools-linux.vsix', + 'checksum': + 'c25299bcfb46b22d41aa3f125df7184e6282a35ff9fb69c47def744cb4778f55', + }, + 'macos': { + 'file_name': 'cpptools-osx-arm64.vsix', + 'checksum': + 'ceb3e8cdaa2b5bb45af50913ddd8402089969748af8d70f5d46480408287ba6f', + }, + 'windows': { + 'file_name': 'cpptools-win32.vsix', + 'checksum': + 'ef7ac5831874a3c7dbf0feb826bfda2be579aff9b6d990622fff1d0d4ede00d1', + "adapters": { + "vscode-cpptools": { + "name": "cppdbg", + "command": [ + "${gadgetDir}/vscode-cpptools/debugAdapters/bin/OpenDebugAD7.exe" + ], + "attach": { + "pidProperty": "processId", + "pidSelect": "ask" + }, + "configuration": { + "type": "cppdbg", + "args": [], + "cwd": "${workspaceRoot}", + "environment": [], + "MIMode": "gdb", + "MIDebuggerPath": "gdb.exe" + } + }, + }, + }, + }, + 'debugpy': { + 'language': 'python', + 'download': { + 'url': 'https://github.com/microsoft/debugpy/archive/${file_name}' + }, + 'all': { + 'version': '1.2.1', + 'file_name': 'v1.2.1.zip', + 'checksum': + '29a6c5d1053d2b6f3b1a63e1a8ecff93f951d3cc0b7548431592e9e3007239e6' + }, + 'do': lambda name, root, gadget: installer.InstallDebugpy( name, + root, + gadget ), + 'adapters': { + 'debugpy': { + "command": [ + sys.executable, # TODO: Will this work from within Vim ? + "${gadgetDir}/debugpy/build/lib/debugpy/adapter" + ], + "name": "debugpy", + "configuration": { + "python": sys.executable, # TODO: Will this work from within Vim ? + # Don't debug into subprocesses, as this leads to problems (vimspector + # doesn't support the custom messages) + # https://github.com/puremourning/vimspector/issues/141 + "subProcess": False, + } + } + }, + }, + 'vscode-java-debug': { + 'language': 'java', + 'enabled': False, + 'download': { + 'url': 'https://github.com/microsoft/vscode-java-debug/releases/download/' + '${version}/${file_name}', + }, + 'all': { + 'version': '0.26.0', + 'file_name': 'vscjava.vscode-java-debug-0.26.0.vsix', + 'checksum': + 'de49116ff3a3c941dad0c36d9af59baa62cd931e808a2ab392056cbb235ad5ef', + }, + 'adapters': { + "vscode-java": { + "name": "vscode-java", + "port": "${DAPPort}", + "configuration": { + "cwd": "${workspaceRoot}" + }, + 'custom_handler': 'vimspector.custom.java.JavaDebugAdapter' + } + }, + }, + 'java-language-server': { + 'language': 'javac', + 'enabled': False, + 'download': { + 'url': 'https://marketplace.visualstudio.com/_apis/public/gallery/' + 'publishers/georgewfraser/vsextensions/vscode-javac/${version}/' + 'vspackage', + 'target': 'georgewfraser.vscode-javac-0.2.31.vsix.gz', + 'format': 'zip.gz', + }, + 'all': { + 'version': '0.2.31', + 'file_name': 'georgewfraser.vscode-javac-0.2.31.vsix.gz', + 'checksum': + '5b0248ec1198d3ece9a9c6b9433b30c22e308f0ae6e4c7bd09cd943c454e3e1d', + }, + 'adapters': { + "vscode-javac": { + "name": "vscode-javac", + "type": "vscode-javac", + "command": [ + "${gadgetDir}/java-language-server/dist/debug_adapter_mac.sh" + ], + "attach": { + "pidSelect": "none" + } + } + }, + }, + 'tclpro': { + 'language': 'tcl', + 'repo': { + 'url': 'https://github.com/puremourning/TclProDebug', + 'ref': 'v1.0.0' + }, + 'do': lambda name, root, gadget: installer.InstallTclProDebug( name, + root, + gadget ), + 'adapters': { + "tclpro": { + "name": "tclpro", + "type": "tclpro", + "command": [ + "${gadgetDir}/tclpro/bin/debugadapter" + ], + "attach": { + "pidSelect": "none" + }, + "configuration": { + "target": "${file}", + "args": [ "*${args}" ], + "tclsh": "tclsh", + "cwd": "${workspaceRoot}", + "extensionDirs": [ + "${workspaceRoot}/.tclpro/extensions", + "${HOME}/.tclpro/extensions", + ] + } + } + }, + }, + 'netcoredbg': { + 'language': [ 'csharp', 'fsharp', 'vbnet' ], + 'enabled': False, + 'download': { + 'url': ( 'https://github.com/Samsung/netcoredbg/releases/download/' + '${version}/${file_name}' ), + 'format': 'tar', + }, + 'all': { + 'version': '1.2.0-782' + }, + 'macos': { + 'file_name': 'netcoredbg-osx.tar.gz', + 'checksum': + '', + }, + 'linux': { + 'file_name': 'netcoredbg-linux-bionic-amd64.tar.gz', + 'checksum': '', + }, + 'windows': { + 'file_name': 'netcoredbg-win64.zip', + 'checksum': '', + }, + 'do': lambda name, root, gadget: installer.MakeSymlink( + name, + os.path.join( root, 'netcoredbg' ) ), + 'adapters': { + 'netcoredbg': { + "name": "netcoredbg", + "command": [ + "${gadgetDir}/netcoredbg/netcoredbg", + "--interpreter=vscode" + ], + "attach": { + "pidProperty": "processId", + "pidSelect": "ask" + }, + "configuration": { + "cwd": "${workspaceRoot}" + } + }, + } + }, + 'vscode-bash-debug': { + 'language': 'bash', + 'download': { + 'url': 'https://github.com/rogalmic/vscode-bash-debug/releases/' + 'download/${version}/${file_name}', + }, + 'all': { + 'file_name': 'bash-debug-0.3.7.vsix', + 'version': 'v0.3.7', + 'checksum': + '7b73e5b4604375df8658fb5a72c645c355785a289aa785a986e508342c014bb4', + }, + 'do': lambda name, root, gadget: installer.InstallBashDebug( name, + root, + gadget ), + 'adapters': { + "vscode-bash": { + "name": "bashdb", + "command": [ + "node", + "${gadgetDir}/vscode-bash-debug/out/bashDebug.js" + ], + "variables": { + "BASHDB_HOME": "${gadgetDir}/vscode-bash-debug/bashdb_dir" + }, + "configuration": { + "request": "launch", + "type": "bashdb", + "program": "${file}", + "args": [], + "env": {}, + "pathBash": "bash", + "pathBashdb": "${BASHDB_HOME}/bashdb", + "pathBashdbLib": "${BASHDB_HOME}", + "pathCat": "cat", + "pathMkfifo": "mkfifo", + "pathPkill": "pkill", + "cwd": "${workspaceRoot}", + "terminalKind": "integrated", + } + } + } + }, + 'vscode-go': { + 'language': 'go', + 'download': { + 'url': 'https://github.com/golang/vscode-go/releases/download/' + 'v${version}/${file_name}' + }, + 'all': { + 'version': '0.19.1', + 'file_name': 'go-0.19.1.vsix', + 'checksum': + '7f9dc014245b030d9f562b28f3ea9b1fd6e2708fac996c53ff6a707f8204ec64', + }, + 'adapters': { + 'vscode-go': { + 'name': 'delve', + 'command': [ + 'node', + '${gadgetDir}/vscode-go/dist/debugAdapter.js' + ], + "configuration": { + "cwd": "${workspaceRoot}", + } + }, + }, + }, + 'vscode-php-debug': { + 'language': 'php', + 'enabled': False, + 'download': { + 'url': + 'https://github.com/xdebug/vscode-php-debug/releases/download/' + '${version}/${file_name}', + }, + 'all': { + 'version': 'v1.17.0', + 'file_name': 'php-debug-1.17.0.vsix', + 'checksum': + 'd0fff272503414b6696cc737bc2e18e060fdd5e5dc4bcaf38ae7373afd8d8bc9', + }, + 'adapters': { + 'vscode-php-debug': { + 'name': "php-debug", + 'command': [ + 'node', + "${gadgetDir}/vscode-php-debug/out/phpDebug.js", + ] + } + } + }, + 'vscode-node-debug2': { + 'language': 'node', + 'enabled': False, + 'repo': { + 'url': 'https://github.com/microsoft/vscode-node-debug2', + 'ref': 'v1.42.5' + }, + 'do': lambda name, root, gadget: installer.InstallNodeDebug( name, + root, + gadget ), + 'adapters': { + 'vscode-node': { + 'name': 'node2', + 'type': 'node2', + 'command': [ + 'node', + '${gadgetDir}/vscode-node-debug2/out/src/nodeDebug.js' + ] + }, + }, + }, + 'debugger-for-chrome': { + 'language': 'chrome', + 'enabled': False, + 'download': { + 'url': 'https://marketplace.visualstudio.com/_apis/public/gallery/' + 'publishers/msjsdiag/vsextensions/' + 'debugger-for-chrome/${version}/vspackage', + 'target': 'msjsdiag.debugger-for-chrome-4.12.10.vsix.gz', + 'format': 'zip.gz', + }, + 'all': { + 'version': '4.12.10', + 'file_name': 'msjsdiag.debugger-for-chrome-4.12.10.vsix', + 'checksum': + '' + }, + 'adapters': { + 'chrome': { + 'name': 'debugger-for-chrome', + 'type': 'chrome', + 'command': [ + 'node', + '${gadgetDir}/debugger-for-chrome/out/src/chromeDebug.js' + ], + }, + }, + }, + 'CodeLLDB': { + 'language': 'rust', + 'enabled': True, + 'download': { + 'url': 'https://github.com/vadimcn/vscode-lldb/releases/download/' + '${version}/${file_name}', + }, + 'all': { + 'version': 'v1.6.6', + }, + 'macos': { + 'file_name': 'codelldb-aarch64-darwin.vsix', + 'checksum': + '5adc3b9139eabdafd825bd5efc55df4424a203fb2b6087b425cd434956e7ec58', + 'make_executable': [ + 'adapter/codelldb', + 'lldb/bin/debugserver', + 'lldb/bin/lldb', + 'lldb/bin/lldb-argdumper', + ], + }, + 'linux': { + 'file_name': 'codelldb-x86_64-linux.vsix', + 'checksum': + 'eda2cd9b3089dcc0524c273e91ffb5875fe08c930bf643739a2cd1846e1f98d6', + 'make_executable': [ + 'adapter/codelldb', + 'lldb/bin/lldb', + 'lldb/bin/lldb-server', + 'lldb/bin/lldb-argdumper', + ], + }, + 'windows': { + 'file_name': 'codelldb-x86_64-windows.vsix', + 'checksum': + '8ddebe8381a3d22dc3d95139c3797fda06b5cc34aadf300e13b1c516b9da95fe', + 'make_executable': [] + }, + 'adapters': { + 'CodeLLDB': { + 'name': 'CodeLLDB', + 'type': 'CodeLLDB', + "command": [ + "${gadgetDir}/CodeLLDB/adapter/codelldb", + "--port", "${unusedLocalPort}" + ], + "port": "${unusedLocalPort}", + "configuration": { + "type": "lldb", + "name": "lldb", + "cargo": {}, + "args": [], + "cwd": "${workspaceRoot}", + "env": {}, + "terminal": "integrated", + } + }, + }, + }, + 'local-lua-debugger-vscode': { + 'language': 'lua', + 'enabled': True, + 'repo': { + 'url': 'https://github.com/tomblind/local-lua-debugger-vscode.git', + 'ref': 'release-${version}' + }, + 'all': { + 'version': '0.2.0', + }, + 'do': lambda name, root, gadget: installer.InstallLuaLocal( name, + root, + gadget ), + 'adapters': { + 'lua-local': { + 'command': [ + 'node', + '${gadgetDir}/local-lua-debugger-vscode/extension/debugAdapter.js' + ], + 'name': 'lua-local', + 'configuration': { + 'interpreter': 'lua', + 'extensionPath': '${gadgetDir}/local-lua-debugger-vscode' + } + } + }, + }, +} diff --git a/python3/vimspector/install.py b/python3/vimspector/install.py index df03a11..d6ceb78 100644 --- a/python3/vimspector/install.py +++ b/python3/vimspector/install.py @@ -26,10 +26,40 @@ def GetOS(): return 'linux' -def GetGadgetDir( vimspector_base, OS ): - return os.path.join( os.path.abspath( vimspector_base ), 'gadgets', OS ) +def mkdirs( p ): + try: + os.makedirs( p ) + except FileExistsError: + pass + + +def MakeInstallDirs( vimspector_base ): + mkdirs( GetGadgetConfigDir( vimspector_base ) ) + mkdirs( GetConfigDirForFiletype( vimspector_base, '_all' ) ) + + +def GetGadgetDir( vimspector_base ): + return os.path.join( os.path.abspath( vimspector_base ), 'gadgets', GetOS() ) + + +def GetManifestFile( vimspector_base ): + return os.path.join( GetGadgetDir( vimspector_base ), + '.gadgets.manifest.json' ) def GetGadgetConfigFile( vimspector_base ): - return os.path.join( GetGadgetDir( vimspector_base, GetOS() ), - '.gadgets.json' ) + return os.path.join( GetGadgetDir( vimspector_base ), '.gadgets.json' ) + + +def GetGadgetConfigDir( vimspector_base ): + return os.path.join( GetGadgetDir( vimspector_base ), '.gadgets.d' ) + + +def GetConfigDirForFiletype( vimspector_base, filetype ): + if not filetype: + filetype = 'default' + + return os.path.join( os.path.abspath( vimspector_base ), + 'configurations', + GetOS(), + filetype ) diff --git a/python3/vimspector/installer.py b/python3/vimspector/installer.py new file mode 100644 index 0000000..a81db8f --- /dev/null +++ b/python3/vimspector/installer.py @@ -0,0 +1,754 @@ +#!/usr/bin/env python3 + +# vimspector - A multi-language debugging system for Vim +# Copyright 2019 Ben Jackson +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from urllib import request +import contextlib +import functools +import gzip +import hashlib +import io +import os +import shutil +import ssl +import string +import subprocess +import sys +import tarfile +import time +import traceback +import zipfile +import json + +from vimspector import install, gadgets + +OUTPUT_VIEW = None + + +class Options: + vimspector_base = None + no_check_certificate = False + quiet = False + + +options = Options() + + +def Configure( **kwargs ): + for k, v in kwargs.items(): + setattr( options, k, v ) + + +def Print( *args, **kwargs ): + if not options.quiet: + print( *args, **kwargs ) + + +class MissingExecutable( Exception ): + pass + + +def GetPATHAsList(): + paths = os.environ[ 'PATH' ].split( os.pathsep ) + if install.GetOS() == 'windows': + paths.insert( 0, os.getcwd() ) + return paths + + +def FindExecutable( executable: str, paths=None ): + if not paths: + paths = GetPATHAsList() + + if install.GetOS() == 'windows': + extensions = [ '.exe', '.bat', '.cmd' ] + else: + extensions = [ '' ] + + for extension in extensions: + if executable.endswith( extension ): + candidate = executable + else: + candidate = executable + extension + + for path in paths: + filename = os.path.abspath( os.path.join( path, candidate ) ) + if not os.path.isfile( filename ): + continue + if not os.access( filename, os.F_OK | os.X_OK ): + continue + + return filename + + raise MissingExecutable( f"Unable to find executable { executable } in path" ) + + + +def CheckCall( cmd, *args, **kwargs ): + cmd[ 0 ] = FindExecutable( cmd[ 0 ] ) + + if options.quiet: + try: + subprocess.check_output( cmd, *args, stderr=subprocess.STDOUT, **kwargs ) + except subprocess.CalledProcessError as e: + print( e.output.decode( 'utf-8' ) ) + raise + else: + subprocess.check_call( cmd, *args, **kwargs ) + + +def PathToAnyWorkingPython3(): + # We can't rely on sys.executable because it's usually 'vim' (fixme, not with + # neovim?) + paths = GetPATHAsList() + + if install.GetOS() == 'windows': + candidates = [ os.path.join( sys.exec_prefix, 'python.exe' ), + 'python.exe' ] + else: + candidates = [ os.path.join( sys.exec_prefix, 'bin', 'python3' ), + 'python3', + 'python' ] + + for candidate in candidates: + try: + return FindExecutable( candidate, paths=paths ) + except MissingExecutable: + pass + + raise RuntimeError( "Unable to find a working python3" ) + + +def RunInstaller( api_prefix, leave_open, *args, **kwargs ): + from vimspector import utils, output, settings + import vim + + if not args: + args = settings.List( 'install_gadgets' ) + + if not args: + return + + args = GadgetListToInstallerArgs( *args ) + + vimspector_home = utils.GetVimValue( vim.vars, 'vimspector_home' ) + vimspector_base_dir = utils.GetVimspectorBase() + + global OUTPUT_VIEW + _ResetInstaller() + + with utils.RestoreCurrentWindow(): + vim.command( f'botright { settings.Int( "bottombar_height" ) }new' ) + win = vim.current.window + OUTPUT_VIEW = output.OutputView( win, api_prefix ) + + cmd = [ + PathToAnyWorkingPython3(), + '-u', + os.path.join( vimspector_home, 'install_gadget.py' ), + '--quiet', + '--update-gadget-config', + ] + if not vimspector_base_dir == vimspector_home: + cmd.extend( [ '--basedir', vimspector_base_dir ] ) + cmd.extend( args ) + + def handler( exit_code ): + if exit_code == 0: + if not leave_open: + _ResetInstaller() + utils.UserMessage( "Vimspector gadget installation complete!" ) + vim.command( 'silent doautocmd User VimspectorInstallSuccess' ) + if 'then' in kwargs: + kwargs[ 'then' ]() + else: + utils.UserMessage( 'Vimspector gadget installation reported errors', + error = True ) + vim.command( 'silent doautocmd User VimspectorInstallFailed' ) + + + OUTPUT_VIEW.RunJobWithOutput( 'Installer', + cmd, + completion_handler = handler, + syntax = 'vimspector-installer' ) + OUTPUT_VIEW.ShowOutput( 'Installer' ) + + +def RunUpdate( api_prefix, leave_open, *args ): + from vimspector import utils, settings + Configure( vimspector_base = utils.GetVimspectorBase() ) + + insatller_args = list( args ) + insatller_args.extend( settings.List( 'install_gadgets' ) ) + + current_adapters = ReadAdapters( read_existing = True ) + for adapter_name in current_adapters.keys(): + insatller_args.extend( FindGadgetForAdapter( adapter_name ) ) + + if insatller_args: + insatller_args.append( '--upgrade' ) + RunInstaller( api_prefix, leave_open, *insatller_args ) + + +def _ResetInstaller(): + global OUTPUT_VIEW + if OUTPUT_VIEW: + OUTPUT_VIEW.Reset() + OUTPUT_VIEW = None + + +def Abort(): + _ResetInstaller() + from vimspector import utils + utils.UserMessage( 'Vimspector installation aborted', + persist = True, + error = True ) + + +def GadgetListToInstallerArgs( *gadget_list ): + installer_args = [] + for name in gadget_list: + if name.startswith( '-' ): + installer_args.append( name ) + continue + + try: + gadget = gadgets.GADGETS[ name ] + except KeyError: + continue + + lang = gadget[ "language" ] + if isinstance( lang, list ): + lang = lang[ 0 ] + + if not gadget.get( 'enabled', True ): + installer_args.append( f'--force-enable-{lang}' ) + else: + installer_args.append( f'--enable-{lang}' ) + + return installer_args + + +def FindGadgetForAdapter( adapter_name ): + candidates = [] + for name, gadget in gadgets.GADGETS.items(): + v = {} + v.update( gadget.get( 'all', {} ) ) + v.update( gadget.get( install.GetOS(), {} ) ) + + adapters = {} + adapters.update( v.get( 'adapters', {} ) ) + adapters.update( gadget.get( 'adapters', {} ) ) + + if adapter_name in adapters: + candidates.append( name ) + + return candidates + + +class Manifest: + manifest: dict + + def __init__( self ): + self.manifest = {} + self.Read() + + def Read( self ): + try: + with open( install.GetManifestFile( options.vimspector_base ), 'r' ) as f: + self.manifest = json.load( f ) + except OSError: + pass + + def Write( self ): + with open( install.GetManifestFile( options.vimspector_base ), 'w' ) as f: + json.dump( self.manifest, f ) + + + def Clear( self, name: str ): + try: + del self.manifest[ name ] + except KeyError: + pass + + + def Update( self, name: str, gadget_spec: dict ): + self.manifest[ name ] = gadget_spec + + + def RequiresUpdate( self, name: str, gadget_spec: dict ): + try: + current_spec = self.manifest[ name ] + except KeyError: + # It's new. + return True + + # If anything changed in the spec, update + if not current_spec == gadget_spec: + return True + + # Always update if the version string is 'master'. Probably a git repo + # that pulls master (which tbh we shouldn't have) + if current_spec.get( 'version' ) in ( 'master', '' ): + return True + if current_spec.get( 'repo', {} ).get( 'ref' ) == 'master': + return True + + return False + + +def ReadAdapters( read_existing = True ): + all_adapters = {} + if read_existing: + try: + with open( install.GetGadgetConfigFile( options.vimspector_base ), + 'r' ) as f: + all_adapters = json.load( f ).get( 'adapters', {} ) + except OSError: + pass + + # Include "built-in" adapter for multi-session mode + all_adapters.update( { + 'multi-session': { + 'port': '${port}', + 'host': '${host}' + }, + } ) + + return all_adapters + + +def WriteAdapters( all_adapters, to_file=None ): + adapter_config = json.dumps ( { 'adapters': all_adapters }, + indent=2, + sort_keys=True ) + + if to_file: + to_file.write( adapter_config ) + else: + with open( install.GetGadgetConfigFile( options.vimspector_base ), + 'w' ) as f: + f.write( adapter_config ) + + +def InstallGeneric( name, root, gadget ): + extension_path = gadget.get( 'extension_path', 'extension' ) + extension = os.path.join( root, extension_path ) + for f in gadget.get( 'make_executable', [] ): + MakeExecutable( os.path.join( extension, f ) ) + + MakeExtensionSymlink( name, root, extension_path ) + + +def InstallCppTools( name, root, gadget ): + extension = os.path.join( root, 'extension' ) + + # It's hilarious, but the execute bits aren't set in the vsix. So they + # actually have javascript code which does this. It's just a horrible horrible + # hack that really is not funny. + MakeExecutable( + os.path.join( extension, 'debugAdapters', 'bin', 'OpenDebugAD7' ) ) + with open( os.path.join( extension, 'package.json' ) ) as f: + package = json.load( f ) + runtime_dependencies = package[ 'runtimeDependencies' ] + for dependency in runtime_dependencies: + for binary in dependency.get( 'binaries' ): + file_path = os.path.abspath( os.path.join( extension, binary ) ) + if os.path.exists( file_path ): + MakeExecutable( os.path.join( extension, binary ) ) + + MakeExtensionSymlink( name, root ) + + +def InstallBashDebug( name, root, gadget ): + MakeExecutable( os.path.join( root, 'extension', 'bashdb_dir', 'bashdb' ) ) + MakeExtensionSymlink( name, root ) + + +def InstallDebugpy( name, root, gadget ): + wd = os.getcwd() + root = os.path.join( root, 'debugpy-{}'.format( gadget[ 'version' ] ) ) + os.chdir( root ) + try: + CheckCall( [ sys.executable, 'setup.py', 'build' ] ) + finally: + os.chdir( wd ) + + MakeSymlink( name, root ) + + +def InstallTclProDebug( name, root, gadget ): + configure = [ 'sh', './configure' ] + + if install.GetOS() == 'macos': + # Apple removed the headers from system frameworks because they are + # determined to make life difficult. And the TCL configure scripts are super + # old so don't know about this. So we do their job for them and try and find + # a tclConfig.sh. + # + # NOTE however that in Apple's infinite wisdom, installing the "headers" in + # the other location is actually broken because the paths in the + # tclConfig.sh are pointing at the _old_ location. You actually do have to + # run the package installation which puts the headers back in order to work. + # This is why the below list is does not contain stuff from + # /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform + # '/Applications/Xcode.app/Contents/Developer/Platforms' + # '/MacOSX.platform/Developer/SDKs/MacOSX.sdk/System' + # '/Library/Frameworks/Tcl.framework', + # '/Applications/Xcode.app/Contents/Developer/Platforms' + # '/MacOSX.platform/Developer/SDKs/MacOSX.sdk/System' + # '/Library/Frameworks/Tcl.framework/Versions' + # '/Current', + for p in [ '/usr/local/opt/tcl-tk/lib' ]: + if os.path.exists( os.path.join( p, 'tclConfig.sh' ) ): + configure.append( '--with-tcl=' + p ) + break + + + with CurrentWorkingDir( os.path.join( root, 'lib', 'tclparser' ) ): + CheckCall( configure ) + CheckCall( [ 'make' ] ) + + MakeSymlink( name, root ) + + +def InstallNodeDebug( name, root, gadget ): + with CurrentWorkingDir( root ): + CheckCall( [ 'npm', 'install' ] ) + CheckCall( [ 'npm', 'run', 'build' ] ) + MakeSymlink( name, root ) + + +def InstallLuaLocal( name, root, gadget ): + with CurrentWorkingDir( root ): + CheckCall( [ 'npm', 'install' ] ) + CheckCall( [ 'npm', 'run', 'build' ] ) + MakeSymlink( name, root ) + + +def InstallGagdet( name: str, + gadget: dict, + manifest: Manifest, + succeeded: list, + failed: list, + all_adapters: dict ): + + try: + # Spec is an os-specific definition of the gadget + spec = {} + spec.update( gadget.get( 'all', {} ) ) + spec.update( gadget.get( install.GetOS(), {} ) ) + + def save_adapters(): + # allow per-os adapter overrides. v already did that for us... + all_adapters.update( spec.get( 'adapters', {} ) ) + # add any other "all" adapters + all_adapters.update( gadget.get( 'adapters', {} ) ) + + if 'download' in gadget: + if 'file_name' not in spec: + raise RuntimeError( "Unsupported OS {} for gadget {}".format( + install.GetOS(), + name ) ) + + print( f"Installing {name}@{spec[ 'version' ]}..." ) + spec[ 'download' ] = gadget[ 'download' ] + if not manifest.RequiresUpdate( name, spec ): + save_adapters() + print( " - Skip - up to date" ) + return + + destination = os.path.join( + install.GetGadgetDir( options.vimspector_base ), + 'download', + name, + spec[ 'version' ] ) + + url = string.Template( gadget[ 'download' ][ 'url' ] ).substitute( spec ) + + file_path = DownloadFileTo( + url, + destination, + file_name = gadget[ 'download' ].get( 'target' ), + checksum = spec.get( 'checksum' ), + check_certificate = not options.no_check_certificate ) + + root = os.path.join( destination, 'root' ) + ExtractZipTo( + file_path, + root, + format = gadget[ 'download' ].get( 'format', 'zip' ) ) + elif 'repo' in gadget: + url = string.Template( gadget[ 'repo' ][ 'url' ] ).substitute( spec ) + ref = string.Template( gadget[ 'repo' ][ 'ref' ] ).substitute( spec ) + + print( f"Installing {name}@{ref}..." ) + spec[ 'repo' ] = gadget[ 'repo' ] + if not manifest.RequiresUpdate( name, spec ): + save_adapters() + print( " - Skip - up to date" ) + return + + destination = os.path.join( + install.GetGadgetDir( options.vimspector_base ), + 'download', + name ) + CloneRepoTo( url, ref, destination ) + root = destination + + if 'do' in gadget: + gadget[ 'do' ]( name, root, spec ) + else: + InstallGeneric( name, root, spec ) + + save_adapters() + manifest.Update( name, spec ) + succeeded.append( name ) + print( f" - Done installing {name}" ) + except Exception as e: + if not options.quiet: + traceback.print_exc() + failed.append( name ) + print( f" - FAILED installing {name}: {e}".format( name, e ) ) + + +@contextlib.contextmanager +def CurrentWorkingDir( d ): + cur_d = os.getcwd() + try: + os.chdir( d ) + yield + finally: + os.chdir( cur_d ) + + +def MakeExecutable( file_path ): + # TODO: import stat and use them by _just_ adding the X bit. + Print( 'Making executable: {}'.format( file_path ) ) + os.chmod( file_path, 0o755 ) + + + +def WithRetry( f ): + retries = 5 + timeout = 1 # seconds + + @functools.wraps( f ) + def wrapper( *args, **kwargs ): + thrown = None + for _ in range( retries ): + try: + return f( *args, **kwargs ) + except Exception as e: + thrown = e + Print( "Failed - {}, will retry in {} seconds".format( e, timeout ) ) + time.sleep( timeout ) + raise thrown + + return wrapper + + +@WithRetry +def UrlOpen( *args, **kwargs ): + return request.urlopen( *args, **kwargs ) + + +def DownloadFileTo( url, + destination, + file_name = None, + checksum = None, + check_certificate = True ): + if not file_name: + file_name = url.split( '/' )[ -1 ] + + file_path = os.path.abspath( os.path.join( destination, file_name ) ) + + if not os.path.isdir( destination ): + os.makedirs( destination ) + + if os.path.exists( file_path ): + if checksum: + if ValidateCheckSumSHA256( file_path, checksum ): + Print( "Checksum matches for {}, using it".format( file_path ) ) + return file_path + else: + Print( "Checksum doesn't match for {}, removing it".format( + file_path ) ) + + Print( "Removing existing {}".format( file_path ) ) + os.remove( file_path ) + + r = request.Request( url, headers = { 'User-Agent': 'Vimspector' } ) + + Print( "Downloading {} to {}/{}".format( url, destination, file_name ) ) + + if not check_certificate: + context = ssl.create_default_context() + context.check_hostname = False + context.verify_mode = ssl.CERT_NONE + kwargs = { "context": context } + else: + kwargs = {} + + with contextlib.closing( UrlOpen( r, **kwargs ) ) as u: + with open( file_path, 'wb' ) as f: + f.write( u.read() ) + + if checksum: + if not ValidateCheckSumSHA256( file_path, checksum ): + raise RuntimeError( + 'Checksum for {} ({}) does not match expected {}'.format( + file_path, + GetChecksumSHA254( file_path ), + checksum ) ) + else: + Print( "Checksum for {}: {}".format( file_path, + GetChecksumSHA254( file_path ) ) ) + + return file_path + + +def GetChecksumSHA254( file_path ): + with open( file_path, 'rb' ) as existing_file: + return hashlib.sha256( existing_file.read() ).hexdigest() + + +def ValidateCheckSumSHA256( file_path, checksum ): + existing_sha256 = GetChecksumSHA254( file_path ) + return existing_sha256 == checksum + + +def RemoveIfExists( destination ): + try: + os.remove( destination ) + Print( "Removed file {}".format( destination ) ) + return + except OSError: + pass + + N = 1 + + + def BackupDir(): + return "{}.{}".format( destination, N ) + + while os.path.isdir( BackupDir() ): + Print( "Removing old dir {}".format( BackupDir() ) ) + try: + shutil.rmtree( BackupDir() ) + Print ( "OK, removed it" ) + break + except OSError as e: + Print ( f"FAILED to remove {BackupDir()}: {e}" ) + N = N + 1 + + if os.path.exists( destination ): + Print( "Removing dir {}".format( destination ) ) + try: + shutil.rmtree( destination ) + except OSError: + Print( "FAILED, moving {} to dir {}".format( destination, BackupDir() ) ) + os.rename( destination, BackupDir() ) + + +# Python's ZipFile module strips execute bits from files, for no good reason +# other than crappy code. Let's do it's job for it. +class ModePreservingZipFile( zipfile.ZipFile ): + def extract( self, member, path = None, pwd = None ): + if not isinstance( member, zipfile.ZipInfo ): + member = self.getinfo( member ) + + if path is None: + path = os.getcwd() + + ret_val = self._extract_member( member, path, pwd ) + attr = member.external_attr >> 16 + os.chmod( ret_val, attr ) + return ret_val + + +def ExtractZipTo( file_path, destination, format ): + Print( "Extracting {} to {}".format( file_path, destination ) ) + RemoveIfExists( destination ) + + if format == 'zip': + with ModePreservingZipFile( file_path ) as f: + f.extractall( path = destination ) + elif format == 'zip.gz': + with gzip.open( file_path, 'rb' ) as f: + file_contents = f.read() + + with ModePreservingZipFile( io.BytesIO( file_contents ) ) as f: + f.extractall( path = destination ) + + elif format == 'tar': + try: + with tarfile.open( file_path ) as f: + f.extractall( path = destination ) + except Exception: + # There seems to a bug in python's tarfile that means it can't read some + # windows-generated tar files + os.makedirs( destination ) + with CurrentWorkingDir( destination ): + CheckCall( [ 'tar', 'zxvf', file_path ] ) + + +def MakeExtensionSymlink( name, root, extension_path = 'extension' ): + MakeSymlink( name, os.path.join( root, extension_path ) ), + + +def MakeSymlink( link, pointing_to, in_folder = None ): + if not in_folder: + in_folder = install.GetGadgetDir( options.vimspector_base ) + + RemoveIfExists( os.path.join( in_folder, link ) ) + + in_folder = os.path.abspath( in_folder ) + pointing_to_relative = os.path.relpath( os.path.abspath( pointing_to ), + in_folder ) + link_path = os.path.join( in_folder, link ) + + if install.GetOS() == 'windows': + # While symlinks do exist on Windows, they require elevated privileges, so + # let's use a directory junction which is all we need. + link_path = os.path.abspath( link_path ) + if os.path.isdir( link_path ): + os.rmdir( link_path ) + CheckCall( [ 'cmd.exe', '/c', 'mklink', '/J', link_path, pointing_to ] ) + else: + os.symlink( pointing_to_relative, link_path ) + + +def CloneRepoTo( url, ref, destination ): + RemoveIfExists( destination ) + git_in_repo = [ 'git', '-C', destination ] + CheckCall( [ 'git', 'clone', url, destination ] ) + CheckCall( git_in_repo + [ 'checkout', ref ] ) + CheckCall( git_in_repo + [ 'submodule', 'sync', '--recursive' ] ) + CheckCall( git_in_repo + [ 'submodule', 'update', '--init', '--recursive' ] ) + + +def AbortIfSUperUser( force_sudo ): + # TODO: We should probably check the effective uid too + is_su = False + if 'SUDO_COMMAND' in os.environ: + is_su = True + + if is_su: + if force_sudo: + print( "*** RUNNING AS SUPER USER DUE TO force_sudo! " + " All bets are off. ***" ) + else: + sys.exit( "This script should *not* be run as super user. Aborting." ) diff --git a/python3/vimspector/output.py b/python3/vimspector/output.py index be722af..3f0da1e 100644 --- a/python3/vimspector/output.py +++ b/python3/vimspector/output.py @@ -13,10 +13,11 @@ # See the License for the specific language governing permissions and # limitations under the License. -from vimspector import utils +from vimspector import utils, install import vim import json +import typing class TabBuffer( object ): @@ -25,13 +26,15 @@ class TabBuffer( object ): self.index = index self.flag = False self.is_job = False + self.syntax = None BUFFER_MAP = { 'console': 'Console', 'stdout': 'Console', + 'output': 'Console', 'stderr': 'stderr', - 'telemetry': 'Telemetry', + 'telemetry': None, } @@ -39,23 +42,34 @@ def CategoryToBuffer( category ): return BUFFER_MAP.get( category, category ) +VIEWS = set() + + +def ShowOutputInWindow( win_id, category ): + for view in VIEWS: + if view._window.valid and utils.WindowID( view._window ) == win_id: + view.ShowOutput( category ) + return + + raise ValueError( f'Unable to find output object for win id {win_id}!' ) + + class OutputView( object ): - def __init__( self, connection, window ): + """Container for a 'tabbed' window of buffers that can be used to display + files or the output of commands.""" + _buffers: typing.Dict[ str, TabBuffer ] + + def __init__( self, window, api_prefix ): self._window = window - self._connection = connection self._buffers = {} + self._api_prefix = api_prefix + VIEWS.add( self ) - for b in set( BUFFER_MAP.values() ): - self._CreateBuffer( b ) + def Print( self, category, text: typing.Union[ str, list ] ): + if not isinstance( text, list ): + text = text.splitlines() - self._CreateBuffer( - 'Vimspector', - file_name = vim.eval( 'expand( "~/.vimspector.log" )' ) ) - - self._ShowOutput( 'Console' ) - - def Print( self, categroy, text ): - self._Print( 'server', text.splitlines() ) + self._Print( category, text ) def OnOutput( self, event ): category = CategoryToBuffer( event.get( 'category' ) or 'output' ) @@ -67,6 +81,10 @@ class OutputView( object ): self._Print( category, text_lines ) def _Print( self, category, text_lines ): + if category is None: + # This category is supressed + return + if category not in self._buffers: self._CreateBuffer( category ) @@ -78,9 +96,187 @@ class OutputView( object ): self._ToggleFlag( category, True ) # Scroll the buffer - with utils.RestoreCurrentWindow(): - with utils.RestoreCurrentBuffer( self._window ): + if self._window.valid: + with utils.RestoreCurrentWindow(): + with utils.RestoreCurrentBuffer( self._window ): + self._ShowOutput( category ) + + def Reset( self ): + self.Clear() + VIEWS.remove( self ) + + + def Clear( self ): + for category, tab_buffer in self._buffers.items(): + self._CleanUpBuffer( category, tab_buffer ) + + # FIXME: nunmenu the WinBar ? + self._buffers = {} + + + def ClearCategory( self, category: str ): + if category not in self._buffers: + return + + self._CleanUpBuffer( category, self._buffers[ category ] ) + + + def _CleanUpBuffer( self, category: str, tab_buffer: TabBuffer ): + if tab_buffer.is_job: + utils.CleanUpCommand( category, self._api_prefix ) + + utils.CleanUpHiddenBuffer( tab_buffer.buf ) + + + def WindowIsValid( self ): + return self._window.valid + + def UseWindow( self, win ): + assert not self._window.valid + self._window = win + # TODO: Sorting of the WinBar ? + for category, _ in self._buffers.items(): + self._RenderWinBar( category ) + + + def _ShowOutput( self, category ): + if not self._window.valid: + return + + utils.JumpToWindow( self._window ) + vim.current.buffer = self._buffers[ category ].buf + vim.command( 'normal G' ) + + def ShowOutput( self, category ): + self._ToggleFlag( category, False ) + self._ShowOutput( category ) + + def _ToggleFlag( self, category, flag ): + if self._buffers[ category ].flag != flag: + self._buffers[ category ].flag = flag + + if self._window.valid: + with utils.LetCurrentWindow( self._window ): + self._RenderWinBar( category ) + + + def RunJobWithOutput( self, category, cmd, **kwargs ): + self._CreateBuffer( category, cmd = cmd, **kwargs ) + + + def _CreateBuffer( self, + category, + file_name = None, + cmd = None, + completion_handler = None, + syntax = None ): + + buf_to_delete = None + if ( not self._buffers + and self._window is not None + and self._window.valid + and not self._window.buffer.name ): + # If there's an empty buffer in the current window that we're not using, + # delete it. We could try and use it, but that complicates the call to + # SetUpCommandBuffer + buf_to_delete = self._window.buffer + + if file_name is not None: + assert cmd is None + if install.GetOS() == "windows": + # FIXME: Can't display fiels in windows (yet?) + return + + cmd = [ 'tail', '-F', '-n', '+1', '--', file_name ] + + if cmd is not None: + out = utils.SetUpCommandBuffer( + cmd, + category, + self._api_prefix, + completion_handler = completion_handler ) + + self._buffers[ category ] = TabBuffer( out, len( self._buffers ) ) + self._buffers[ category ].is_job = True + self._RenderWinBar( category ) + else: + if category == 'Console': + name = 'vimspector.Console' + else: + name = 'vimspector.Output:{0}'.format( category ) + + tab_buffer = TabBuffer( utils.NewEmptyBuffer(), len( self._buffers ) ) + + self._buffers[ category ] = tab_buffer + + if category == 'Console': + utils.SetUpPromptBuffer( tab_buffer.buf, + name, + '> ', + 'vimspector#EvaluateConsole', + 'vimspector#OmniFuncConsole' ) + else: + utils.SetUpHiddenBuffer( tab_buffer.buf, name ) + + self._RenderWinBar( category ) + + self._buffers[ category ].syntax = utils.SetSyntax( + self._buffers[ category ].syntax, + syntax, + self._buffers[ category ].buf ) + + if buf_to_delete: + with utils.RestoreCurrentWindow(): self._ShowOutput( category ) + utils.CleanUpHiddenBuffer( buf_to_delete ) + + def _RenderWinBar( self, category ): + if not utils.UseWinBar(): + return + + if not self._window.valid: + return + + with utils.LetCurrentWindow( self._window ): + tab_buffer = self._buffers[ category ] + + try: + if tab_buffer.flag: + vim.command( 'nunmenu WinBar.{}'.format( utils.Escape( category ) ) ) + else: + vim.command( 'nunmenu WinBar.{}*'.format( utils.Escape( category ) ) ) + except vim.error as e: + # E329 means the menu doesn't exist; ignore that. + if 'E329' not in str( e ): + raise + + vim.command( + "nnoremenu 1.{0} WinBar.{1}{2} " + ":call vimspector#ShowOutputInWindow( {3}, '{1}' )".format( + tab_buffer.index, + utils.Escape( category ), + '*' if tab_buffer.flag else '', + utils.WindowID( self._window ) ) ) + + def GetCategories( self ): + return list( self._buffers.keys() ) + + def AddLogFileView( self, file_name = utils.LOG_FILE ): + self._CreateBuffer( 'Vimspector', file_name = file_name ) + + +class DAPOutputView( OutputView ): + """Specialised OutputView which adds the DAP Console (REPL)""" + def __init__( self, *args ): + super().__init__( *args ) + + self._connection = None + for b in set( BUFFER_MAP.values() ): + if b is not None: + self._CreateBuffer( b ) + + self.AddLogFileView() + self._ShowOutput( 'Console' ) def ConnectionUp( self, connection ): self._connection = connection @@ -89,119 +285,30 @@ class OutputView( object ): # Don't clear because output is probably still useful self._connection = None - def Reset( self ): - self.Clear() - - def Clear( self ): - for category, tab_buffer in self._buffers.items(): - if tab_buffer.is_job: - utils.CleanUpCommand( category ) - try: - vim.command( 'bdelete! {0}'.format( tab_buffer.buf.number ) ) - except vim.error as e: - # FIXME: For now just ignore the "no buffers were deleted" error - if 'E516' not in e: - raise - - self._buffers = {} - - def _ShowOutput( self, category ): - utils.JumpToWindow( self._window ) - vim.command( 'bu {0}'.format( self._buffers[ category ].buf.name ) ) - vim.command( 'normal G' ) - - def ShowOutput( self, category ): - self._ToggleFlag( category, False ) - self._ShowOutput( category ) - - def Evaluate( self, frame, expression ): - if not frame: - self.Print( 'Console', 'There is no current stack frame' ) - return - - console = self._buffers[ 'Console' ].buf - utils.AppendToBuffer( console, 'Evaluating: ' + expression ) + def Evaluate( self, frame, expression, verbose ): + if verbose: + self._Print( 'Console', f"Evaluating: { expression }" ) def print_result( message ): - utils.AppendToBuffer( console, - 'Evaluated: ' + expression ) - result = message[ 'body' ][ 'result' ] if result is None: - result = 'null' + result = '' + self._Print( 'Console', result.splitlines() ) - utils.AppendToBuffer( console, ' Result: ' + result ) + def print_failure( reason, msg ): + self._Print( 'Console', reason.splitlines() ) - self._connection.DoRequest( print_result, { + request = { 'command': 'evaluate', 'arguments': { 'expression': expression, 'context': 'repl', - 'frameId': frame[ 'id' ], } - } ) + } - def _ToggleFlag( self, category, flag ): - if self._buffers[ category ].flag != flag: - self._buffers[ category ].flag = flag - with utils.LetCurrentWindow( self._window ): - self._RenderWinBar( category ) + if frame: + request[ 'arguments' ][ 'frameId' ] = frame[ 'id' ] - - def RunJobWithOutput( self, category, cmd ): - self._CreateBuffer( category, cmd = cmd ) - - - def _CreateBuffer( self, category, file_name = None, cmd = None ): - with utils.LetCurrentWindow( self._window ): - with utils.RestoreCurrentBuffer( self._window ): - - if file_name is not None: - assert cmd is None - cmd = [ 'tail', '-F', '-n', '+1', '--', file_name ] - - if cmd is not None: - out, err = utils.SetUpCommandBuffer( cmd, category ) - self._buffers[ category + '-out' ] = TabBuffer( out, - len( self._buffers ) ) - self._buffers[ category + '-out' ].is_job = True - self._buffers[ category + '-err' ] = TabBuffer( err, - len( self._buffers ) ) - self._buffers[ category + '-err' ].is_job = False - self._RenderWinBar( category + '-out' ) - self._RenderWinBar( category + '-err' ) - else: - vim.command( 'enew' ) - tab_buffer = TabBuffer( vim.current.buffer, len( self._buffers ) ) - self._buffers[ category ] = tab_buffer - if category == 'Console': - utils.SetUpPromptBuffer( tab_buffer.buf, - 'vimspector.Console', - '> ', - 'vimspector#EvaluateConsole', - hidden=True ) - else: - utils.SetUpHiddenBuffer( - tab_buffer.buf, - 'vimspector.Output:{0}'.format( category ) ) - - self._RenderWinBar( category ) - - def _RenderWinBar( self, category ): - tab_buffer = self._buffers[ category ] - - try: - if tab_buffer.flag: - vim.command( 'nunmenu WinBar.{}'.format( utils.Escape( category ) ) ) - else: - vim.command( 'nunmenu WinBar.{}*'.format( utils.Escape( category ) ) ) - except vim.error as e: - # E329 means the menu doesn't exist; ignore that. - if 'E329' not in str( e ): - raise - - vim.command( "nnoremenu 1.{0} WinBar.{1}{2} " - ":call vimspector#ShowOutput( '{1}' )".format( - tab_buffer.index, - utils.Escape( category ), - '*' if tab_buffer.flag else '' ) ) + self._connection.DoRequest( print_result, + request, + print_failure ) diff --git a/python3/vimspector/settings.py b/python3/vimspector/settings.py new file mode 100644 index 0000000..89378af --- /dev/null +++ b/python3/vimspector/settings.py @@ -0,0 +1,138 @@ +# vimspector - A multi-language debugging system for Vim +# Copyright 2020 Ben Jackson +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +import vim +import builtins +from vimspector import utils + +DEFAULTS = { + # UI + 'ui_mode': 'auto', + 'bottombar_height': 10, + + # For ui_mode = 'horizontal': + 'sidebar_width': 50, + 'code_minwidth': 82, + 'terminal_maxwidth': 80, + 'terminal_minwidth': 10, + + # For ui_mode = 'vertical': + 'topbar_height': 15, + 'code_minheight': 20, + 'terminal_maxheight': 15, + 'terminal_minheight': 5, + + # Signs + 'sign_priority': { + 'vimspectorPC': 200, + 'vimspectorPCBP': 200, + 'vimspectorBP': 9, + 'vimspectorBPCond': 9, + 'vimspectorBPDisabled': 9, + 'vimspectorCurrentThread': 200, + 'vimspectorCurrentFrame': 200, + }, + + # Installer + 'install_gadgets': [], + + # Mappings + 'mappings': { + 'variables': { + 'expand_collapse': [ '', '<2-LeftMouse>' ], + 'delete': [ '' ], + 'set_value': [ '', '' ] + }, + 'stack_trace': { + 'expand_or_jump': [ '', '<2-LeftMouse>' ], + 'focus_thread': [ '' ], + } + }, + + # Custom + 'java_hotcodereplace_mode': 'ask', +} + + +def Get( option: str, default=None, cls=str ): + return cls( utils.GetVimValue( vim.vars, + f'vimspector_{ option }', + DEFAULTS.get( option, cls() ) ) ) + + +def Int( option: str ): + return Get( option, cls=builtins.int ) + + +def List( option: str ): + return utils.GetVimList( vim.vars, + f'vimspector_{ option }', + DEFAULTS.get( option, [] ) ) + + +# FIXME: +# In Vim, we must use vim.Dictionary because this sorts out the annoying +# keys-as-bytes discrepancy, making things awkward. That said, we still have the +# problem where the _values_ are potentially bytes. It's very tempting to just +# make a deep copy to antive str type here. +# Of course in neovim, it's totally different and you actually get a dict type +# back (though for once, neovim is making life somewhat easier for a change). +DICT_TYPE = dict +if hasattr( vim, 'Dictionary' ): + DICT_TYPE = vim.Dictionary + + +def Dict( option ): + return _UpdateDict( DICT_TYPE( DEFAULTS.get( option, {} ) ), + vim.vars.get( f'vimspector_{ option }', DICT_TYPE() ) ) + + +def _UpdateDict( target, override ): + """Apply the updates in |override| to the dict |target|. This is like + dict.update, but recursive. i.e. if the existing element is a dict, then + override elements of the sub-dict rather than wholesale replacing. + e.g. + UpdateDict( + { + 'outer': { 'inner': { 'key': 'oldValue', 'existingKey': True } } + }, + { + 'outer': { 'inner': { 'key': 'newValue' } }, + 'newKey': { 'newDict': True }, + } + ) + yields: + { + 'outer': { + 'inner': { + 'key': 'newValue', + 'existingKey': True + } + }, + 'newKey': { newDict: True } + } + """ + + for key, value in override.items(): + current_value = target.get( key ) + if not isinstance( current_value, DICT_TYPE ): + target[ key ] = value + elif isinstance( value, DICT_TYPE ): + target[ key ] = _UpdateDict( current_value, value ) + else: + target[ key ] = value + + return target diff --git a/python3/vimspector/signs.py b/python3/vimspector/signs.py new file mode 100644 index 0000000..39d1f03 --- /dev/null +++ b/python3/vimspector/signs.py @@ -0,0 +1,46 @@ +import vim + +from vimspector import settings, utils + + +def SignDefined( name ): + if utils.Exists( "*sign_getdefined" ): + return int( + vim.eval( f"len( sign_getdefined( '{ utils.Escape( name ) }' ) )" ) + ) + + return False + + +def DefineSign( name, text, double_text, texthl, col = 'right', **kwargs ): + if utils.GetVimValue( vim.options, 'ambiwidth', '' ) == 'double': + text = double_text + + if col == 'right': + if int( utils.Call( 'strdisplaywidth', text ) ) < 2: + text = ' ' + text + + text = text.replace( ' ', r'\ ' ) + + cmd = f'sign define { name } text={ text } texthl={ texthl }' + for key, value in kwargs.items(): + cmd += f' { key }={ value }' + + vim.command( cmd ) + + +def PlaceSign( sign_id, group, name, file_name, line ): + priority = settings.Dict( 'sign_priority' )[ name ] + + cmd = ( f'sign place { sign_id } ' + f'group={ group } ' + f'name={ name } ' + f'priority={ priority } ' + f'line={ line } ' + f'file={ file_name }' ) + + vim.command( cmd ) + + +def UnplaceSign( sign_id, group ): + vim.command( f'sign unplace { sign_id } group={ group }' ) diff --git a/python3/vimspector/stack_trace.py b/python3/vimspector/stack_trace.py index 74bac68..8b1d848 100644 --- a/python3/vimspector/stack_trace.py +++ b/python3/vimspector/stack_trace.py @@ -16,59 +16,168 @@ import vim import os import logging +import typing -from vimspector import utils +from vimspector import utils, signs, settings + + +class Thread: + """The state of a single thread.""" + PAUSED = 0 + RUNNING = 1 + TERMINATED = 3 + state = RUNNING + + stopped_event: typing.Dict + thread: typing.Dict + stacktrace: typing.List[ typing.Dict ] + id: str + + def __init__( self, thread ): + self.id = thread[ 'id' ] + self.stopped_event = None + self.Update( thread ) + + def Update( self, thread ): + self.thread = thread + self.stacktrace = None + + def Paused( self, event ): + self.state = Thread.PAUSED + self.stopped_event = event + + def Continued( self ): + self.state = Thread.RUNNING + self.stopped_event = None + self.Collapse() + + def Exited( self ): + self.state = Thread.TERMINATED + self.stopped_event = None + + def State( self ): + if self.state == Thread.PAUSED: + return self.stopped_event.get( 'description' ) or 'paused' + elif self.state == Thread.RUNNING: + return 'running' + return 'terminated' + + def Expand( self, stack_trace ): + self.stacktrace = stack_trace + + def Collapse( self ): + self.stacktrace = None + + def IsExpanded( self ): + return self.stacktrace is not None + + def CanExpand( self ): + return self.state == Thread.PAUSED class StackTraceView( object ): - def __init__( self, session, connection, buf ): + class ThreadRequestState: + NO = 0 + REQUESTING = 1 + PENDING = 2 + + # FIXME: Make into a dict by id ? + _threads: typing.List[ Thread ] + _line_to_thread = typing.Dict[ int, Thread ] + + def __init__( self, session, win ): self._logger = logging.getLogger( __name__ ) utils.SetUpLogging( self._logger ) - self._buf = buf + self._buf = win.buffer self._session = session - self._connection = connection + self._connection = None - self._currentThread = None - self._currentFrame = None + self._current_thread = None + self._current_frame = None + self._current_syntax = "" self._threads = [] self._sources = {} + self._scratch_buffers = [] - utils.SetUpScratchBuffer( self._buf, 'vimspector.StackTrace' ) - vim.current.buffer = self._buf - vim.command( 'nnoremap :call vimspector#GoToFrame()' ) + # FIXME: This ID is by group, so should be module scope + self._current_thread_sign_id = 0 # 1 when used + self._current_frame_sign_id = 0 # 2 when used + + utils.SetUpHiddenBuffer( self._buf, 'vimspector.StackTrace' ) + utils.SetUpUIWindow( win ) + + mappings = settings.Dict( 'mappings' )[ 'stack_trace' ] + + with utils.LetCurrentWindow( win ): + for mapping in utils.GetVimList( mappings, 'expand_or_jump' ): + vim.command( f'nnoremap { mapping } ' + ':call vimspector#GoToFrame()' ) + + for mapping in utils.GetVimList( mappings, 'focus_thread' ): + vim.command( f'nnoremap { mapping } ' + ':call vimspector#SetCurrentThread()' ) + + if utils.UseWinBar(): + vim.command( 'nnoremenu 1.1 WinBar.Pause/Continue ' + ':call vimspector#PauseContinueThread()' ) + vim.command( 'nnoremenu 1.2 WinBar.Expand/Collapse ' + ':call vimspector#GoToFrame()' ) + vim.command( 'nnoremenu 1.3 WinBar.Focus ' + ':call vimspector#SetCurrentThread()' ) + + win.options[ 'cursorline' ] = False + win.options[ 'signcolumn' ] = 'auto' + + + if not signs.SignDefined( 'vimspectorCurrentThread' ): + signs.DefineSign( 'vimspectorCurrentThread', + text = '▶ ', + double_text = '▶', + texthl = 'MatchParen', + linehl = 'CursorLine' ) + + if not signs.SignDefined( 'vimspectorCurrentFrame' ): + signs.DefineSign( 'vimspectorCurrentFrame', + text = '▶ ', + double_text = '▶', + texthl = 'Special', + linehl = 'CursorLine' ) self._line_to_frame = {} self._line_to_thread = {} - # TODO: We really need a proper state model - # - # AWAIT_CONNECTION -- OnServerReady / RequestThreads --> REQUESTING_THREADS - # REQUESTING -- OnGotThreads / RequestScopes --> REQUESTING_SCOPES - # - # When we attach using gdbserver, this whole thing breaks because we request - # the threads over and over and get duff data back on later threads. - self._requesting_threads = False + self._requesting_threads = StackTraceView.ThreadRequestState.NO + self._pending_thread_request = None def GetCurrentThreadId( self ): - return self._currentThread + return self._current_thread def GetCurrentFrame( self ): - return self._currentFrame + return self._current_frame def Clear( self ): - self._currentFrame = None - self._currentThread = None - self._threads = [] + self._current_frame = None + self._current_thread = None + self._current_syntax = "" + self._threads.clear() self._sources = {} + self._requesting_threads = StackTraceView.ThreadRequestState.NO + self._pending_thread_request = None + if self._current_thread_sign_id: + signs.UnplaceSign( self._current_thread_sign_id, 'VimspectorStackTrace' ) + self._current_thread_sign_id = 0 + if self._current_frame_sign_id: + signs.UnplaceSign( self._current_frame_sign_id, 'VimspectorStackTrace' ) + self._current_frame_sign_id = 0 + with utils.ModifiableScratchBuffer( self._buf ): utils.ClearBuffer( self._buf ) def ConnectionUp( self, connection ): self._connection = connection - self._requesting_threads = False def ConnectionClosed( self ): self.Clear() @@ -76,69 +185,142 @@ class StackTraceView( object ): def Reset( self ): self.Clear() - # TODO: delete the buffer ? + utils.CleanUpHiddenBuffer( self._buf ) + for b in self._scratch_buffers: + utils.CleanUpHiddenBuffer( b ) + self._scratch_buffers = [] + self._buf = None - def LoadThreads( self, infer_current_frame ): - pending_request = False - if self._requesting_threads: - pending_request = True + def LoadThreads( self, + infer_current_frame, + reason = '', + stopEvent = None ): + if self._requesting_threads != StackTraceView.ThreadRequestState.NO: + self._requesting_threads = StackTraceView.ThreadRequestState.PENDING + self._pending_thread_request = ( infer_current_frame, + reason, + stopEvent ) return def consume_threads( message ): - self._requesting_threads = False + requesting = False + if self._requesting_threads == StackTraceView.ThreadRequestState.PENDING: + # We may have hit a thread event, so try again. + self._requesting_threads = StackTraceView.ThreadRequestState.NO + self.LoadThreads( *self._pending_thread_request ) + requesting = True - if not message[ 'body' ][ 'threads' ]: - if pending_request: - # We may have hit a thread event, so try again. - self.LoadThreads( infer_current_frame ) - return - else: - # This is a protocol error. It is required to return at least one! - utils.UserMessage( 'Server returned no threads. Is it running?', - persist = True ) + self._requesting_threads = StackTraceView.ThreadRequestState.NO + self._pending_thread_request = None + if not ( message.get( 'body' ) or {} ).get( 'threads' ): + # This is a protocol error. It is required to return at least one! + utils.UserMessage( 'Protocol error: Server returned no threads', + persist = False, + error = True ) + return + + existing_threads = self._threads[ : ] self._threads.clear() - for thread in message[ 'body' ][ 'threads' ]: + if stopEvent is not None: + stoppedThreadId = stopEvent.get( 'threadId' ) + allThreadsStopped = stopEvent.get( 'allThreadsStopped', False ) + + # FIXME: This is horribly inefficient + for t in message[ 'body' ][ 'threads' ]: + thread = None + for existing_thread in existing_threads: + if existing_thread.id == t[ 'id' ]: + thread = existing_thread + thread.Update( t ) + break + + if not thread: + thread = Thread( t ) + self._threads.append( thread ) - if infer_current_frame and thread[ 'id' ] == self._currentThread: - self._LoadStackTrace( thread, True ) - elif infer_current_frame and self._currentThread is None: - self._currentThread = thread[ 'id' ] - self._LoadStackTrace( thread, True ) + # If the threads were requested due to a stopped event, update any + # stopped thread state. Note we have to do this here (rather than in the + # stopped event handler) because we must apply this event to any new + # threads that are received here. + if stopEvent: + if allThreadsStopped: + thread.Paused( stopEvent ) + elif stoppedThreadId is not None and thread.id == stoppedThreadId: + thread.Paused( stopEvent ) - self._DrawThreads() + # If this is a stopped event, load the stack trace for the "current" + # thread. Don't do this on other thrads requests because some servers + # just break when that happens. + # + # Don't do this if we're also satisfying a cached request already (we'll + # do it then) + if infer_current_frame and not requesting: + if thread.id == self._current_thread: + if thread.CanExpand(): + self._LoadStackTrace( thread, True, reason ) + requesting = True + elif self._current_thread is None: + self._current_thread = thread.id + if thread.CanExpand(): + self._LoadStackTrace( thread, True, reason ) + requesting = True - self._requesting_threads = True + if not requesting: + self._DrawThreads() + + def failure_handler( reason, msg ): + # Make sure we request them again if the request fails + self._requesting_threads = StackTraceView.ThreadRequestState.NO + self._pending_thread_request = None + + self._requesting_threads = StackTraceView.ThreadRequestState.REQUESTING self._connection.DoRequest( consume_threads, { 'command': 'threads', - } ) + }, failure_handler ) def _DrawThreads( self ): self._line_to_frame.clear() self._line_to_thread.clear() + if self._current_thread_sign_id: + signs.UnplaceSign( self._current_thread_sign_id, 'VimspectorStackTrace' ) + else: + self._current_thread_sign_id = 1 + with utils.ModifiableScratchBuffer( self._buf ): - utils.ClearBuffer( self._buf ) + with utils.RestoreCursorPosition(): + utils.ClearBuffer( self._buf ) - for thread in self._threads: - icon = '+' if '_frames' not in thread else '-' + for thread in self._threads: + icon = '+' if not thread.IsExpanded() else '-' + line = utils.AppendToBuffer( + self._buf, + f'{icon} Thread {thread.id}: {thread.thread["name"]} ' + f'({thread.State()})' ) - line = utils.AppendToBuffer( - self._buf, - '{0} Thread: {1}'.format( icon, thread[ 'name' ] ) ) + if self._current_thread == thread.id: + signs.PlaceSign( self._current_thread_sign_id, + 'VimspectorStackTrace', + 'vimspectorCurrentThread', + self._buf.name, + line ) - self._line_to_thread[ line ] = thread + self._line_to_thread[ line ] = thread + self._DrawStackTrace( thread ) - self._DrawStackTrace( thread ) + def _LoadStackTrace( self, + thread: Thread, + infer_current_frame, + reason = '' ): - def _LoadStackTrace( self, thread, infer_current_frame ): def consume_stacktrace( message ): - thread[ '_frames' ] = message[ 'body' ][ 'stackFrames' ] + thread.Expand( message[ 'body' ][ 'stackFrames' ] ) if infer_current_frame: - for frame in thread[ '_frames' ]: - if self._JumpToFrame( frame ): + for frame in thread.stacktrace: + if self._JumpToFrame( frame, reason ): break self._DrawThreads() @@ -146,34 +328,116 @@ class StackTraceView( object ): self._connection.DoRequest( consume_stacktrace, { 'command': 'stackTrace', 'arguments': { - 'threadId': thread[ 'id' ], + 'threadId': thread.id, } } ) - def ExpandFrameOrThread( self ): + + def _GetSelectedThread( self ) -> Thread: if vim.current.buffer != self._buf: + return None + + return self._line_to_thread.get( vim.current.window.cursor[ 0 ] ) + + + def GetSelectedThreadId( self ): + thread = self._GetSelectedThread() + return thread.id if thread else thread + + def _SetCurrentThread( self, thread: Thread ): + self._current_thread = thread.id + self._DrawThreads() + + def SetCurrentThread( self ): + thread = self._GetSelectedThread() + if thread: + self._SetCurrentThread( thread ) + elif vim.current.buffer != self._buf: return + elif vim.current.window.cursor[ 0 ] in self._line_to_frame: + thread, frame = self._line_to_frame[ vim.current.window.cursor[ 0 ] ] + self._SetCurrentThread( thread ) + self._JumpToFrame( frame ) + else: + utils.UserMessage( "No thread selected" ) - current_line = vim.current.window.cursor[ 0 ] + def ExpandFrameOrThread( self ): + thread = self._GetSelectedThread() - if current_line in self._line_to_frame: - self._JumpToFrame( self._line_to_frame[ current_line ] ) - elif current_line in self._line_to_thread: - thread = self._line_to_thread[ current_line ] - if '_frames' in thread: - del thread[ '_frames' ] - with utils.RestoreCursorPosition(): - self._DrawThreads() - else: + if thread: + if thread.IsExpanded(): + thread.Collapse() + self._DrawThreads() + elif thread.CanExpand(): self._LoadStackTrace( thread, False ) + else: + utils.UserMessage( "Thread is not stopped" ) + elif vim.current.buffer != self._buf: + return + elif vim.current.window.cursor[ 0 ] in self._line_to_frame: + thread, frame = self._line_to_frame[ vim.current.window.cursor[ 0 ] ] + self._JumpToFrame( frame ) - def _JumpToFrame( self, frame ): + + + def _GetFrameOffset( self, delta ): + thread: Thread + for thread in self._threads: + if thread.id != self._current_thread: + continue + + if not thread.stacktrace: + return + + frame_idx = None + for index, frame in enumerate( thread.stacktrace ): + if frame == self._current_frame: + frame_idx = index + break + + if frame_idx is not None: + target_idx = frame_idx + delta + if target_idx >= 0 and target_idx < len( thread.stacktrace ): + return thread.stacktrace[ target_idx ] + + break + + + def UpFrame( self ): + frame = self._GetFrameOffset( 1 ) + if not frame: + utils.UserMessage( 'Top of stack' ) + else: + self._JumpToFrame( frame, 'up' ) + + + def DownFrame( self ): + frame = self._GetFrameOffset( -1 ) + if not frame: + utils.UserMessage( 'Bottom of stack' ) + else: + self._JumpToFrame( frame, 'down' ) + + + def AnyThreadsRunning( self ): + for thread in self._threads: + if thread.state != Thread.TERMINATED: + return True + + return False + + + def _JumpToFrame( self, frame, reason = '' ): def do_jump(): - if 'line' in frame and frame[ 'line' ]: - self._currentFrame = frame - return self._session.SetCurrentFrame( self._currentFrame ) + if 'line' in frame and frame[ 'line' ] > 0: + # Should this set the current _Thread_ too ? If i jump to a frame in + # Thread 2, should that become the focussed thread ? + self._current_frame = frame + self._DrawThreads() + return self._session.SetCurrentFrame( self._current_frame, reason ) + return False - source = frame[ 'source' ] + source = frame.get( 'source' ) or {} if source.get( 'sourceReference', 0 ) > 0: def handle_resolved_source( resolved_source ): frame[ 'source' ] = resolved_source @@ -185,59 +449,94 @@ class StackTraceView( object ): else: return do_jump() + + def PauseContinueThread( self ): + thread = self._GetSelectedThread() + if thread is None: + utils.UserMessage( 'No thread selected' ) + elif thread.state == Thread.PAUSED: + self._session._connection.DoRequest( + lambda msg: self.OnContinued( { + 'threadId': thread.id, + 'allThreadsContinued': ( msg.get( 'body' ) or {} ).get( + 'allThreadsContinued', + True ) + } ), + { + 'command': 'continue', + 'arguments': { + 'threadId': thread.id, + }, + } ) + elif thread.state == Thread.RUNNING: + self._session._connection.DoRequest( None, { + 'command': 'pause', + 'arguments': { + 'threadId': thread.id, + }, + } ) + else: + utils.UserMessage( + f'Thread cannot be modified in state {thread.State()}' ) + + + def OnContinued( self, event = None ): + threadId = None + allThreadsContinued = True + + if event is not None: + threadId = event[ 'threadId' ] + allThreadsContinued = event.get( 'allThreadsContinued', False ) + + for thread in self._threads: + if allThreadsContinued: + thread.Continued() + elif thread.id == threadId: + thread.Continued() + break + + self._DrawThreads() + def OnStopped( self, event ): - if 'threadId' in event: - self._currentThread = event[ 'threadId' ] - elif event.get( 'allThreadsStopped', False ) and self._threads: - self._currentThread = self._threads[ 0 ][ 'id' ] + threadId = event.get( 'threadId' ) + allThreadsStopped = event.get( 'allThreadsStopped', False ) - if self._currentThread is not None: - for thread in self._threads: - if thread[ 'id' ] == self._currentThread: - self._LoadStackTrace( thread, True ) - return + # Work out if we should change the current thread + if threadId is not None: + self._current_thread = threadId + elif self._current_thread is None and allThreadsStopped and self._threads: + self._current_thread = self._threads[ 0 ].id - self.LoadThreads( True ) + self.LoadThreads( True, 'stopped', event ) def OnThreadEvent( self, event ): - if event[ 'reason' ] == 'started' and self._currentThread is None: - self._currentThread = event[ 'threadId' ] - self.LoadThreads( True ) + infer_current_frame = False + if event[ 'reason' ] == 'started' and self._current_thread is None: + self._current_thread = event[ 'threadId' ] + infer_current_frame = True - def Continue( self ): - if self._currentThread is None: - utils.UserMessage( 'No current thread', persist = True ) + if event[ 'reason' ] == 'exited': + for thread in self._threads: + if thread.id == event[ 'threadId' ]: + thread.Exited() + break + + self.LoadThreads( infer_current_frame ) + + def OnExited( self, event ): + for thread in self._threads: + thread.Exited() + + def _DrawStackTrace( self, thread: Thread ): + if not thread.IsExpanded(): return - self._session._connection.DoRequest( None, { - 'command': 'continue', - 'arguments': { - 'threadId': self._currentThread, - }, - } ) + if self._current_frame_sign_id: + signs.UnplaceSign( self._current_frame_sign_id, 'VimspectorStackTrace' ) + else: + self._current_frame_sign_id = 2 - self._session.ClearCurrentFrame() - self.LoadThreads( True ) - - def Pause( self ): - if self._currentThread is None: - utils.UserMessage( 'No current thread', persist = True ) - return - - self._session._connection.DoRequest( None, { - 'command': 'pause', - 'arguments': { - 'threadId': self._currentThread, - }, - } ) - - def _DrawStackTrace( self, thread ): - if '_frames' not in thread: - return - - stackFrames = thread[ '_frames' ] - - for frame in stackFrames: + for frame in thread.stacktrace: if frame.get( 'source' ): source = frame[ 'source' ] else: @@ -261,7 +560,15 @@ class StackTraceView( object ): source[ 'name' ], frame[ 'line' ] ) ) - self._line_to_frame[ line ] = frame + if ( self._current_frame is not None and + self._current_frame[ 'id' ] == frame[ 'id' ] ): + signs.PlaceSign( self._current_frame_sign_id, + 'VimspectorStackTrace', + 'vimspectorCurrentFrame', + self._buf.name, + line ) + + self._line_to_frame[ line ] = ( thread, frame ) def _ResolveSource( self, source, and_then ): source_reference = int( source[ 'sourceReference' ] ) @@ -274,12 +581,14 @@ class StackTraceView( object ): def consume_source( msg ): self._sources[ source_reference ] = source - buf_name = os.path.join( '_vimspector_tmp', source[ 'name' ] ) + buf_name = os.path.join( '_vimspector_tmp', + source.get( 'path', source[ 'name' ] ) ) self._logger.debug( "Received source %s: %s", buf_name, msg ) buf = utils.BufferForFile( buf_name ) - utils.SetUpScratchBuffer( buf, buf_name ) + self._scratch_buffers.append( buf ) + utils.SetUpHiddenBuffer( buf, buf_name ) source[ 'path' ] = buf_name with utils.ModifiableScratchBuffer( buf ): utils.SetBufferContents( buf, msg[ 'body' ][ 'content' ] ) @@ -293,3 +602,8 @@ class StackTraceView( object ): 'source': source } } ) + + def SetSyntax( self, syntax ): + self._current_syntax = utils.SetSyntax( self._current_syntax, + syntax, + self._buf ) diff --git a/python3/vimspector/terminal.py b/python3/vimspector/terminal.py new file mode 100644 index 0000000..0c84704 --- /dev/null +++ b/python3/vimspector/terminal.py @@ -0,0 +1,134 @@ +from vimspector import utils, settings + +import os +import vim + + +class Terminal: + window = None + buffer_number: int = None + + +def LaunchTerminal( api_prefix, + params, + window_for_start, + existing_term ): + if not existing_term: + term = Terminal() + else: + term = existing_term + + cwd = params[ 'cwd' ] or os.getcwd() + args = params[ 'args' ] or [] + env = params.get( 'env' ) or {} + + term_options = { + 'norestore': 1, + 'cwd': cwd, + 'env': env, + } + + if settings.Get( 'ui_mode' ) == 'horizontal': + # force-horizontal + term_options[ 'vertical' ] = 1 + elif utils.GetVimValue( vim.vars[ 'vimspector_session_windows' ], + 'mode' ) == 'horizontal': + # horizontal, which means that we should have enough space for: + # - sidebar + # - code min + # - term min width + # - + 2 vertical spaders + # - + 3 columns for signs + term_options[ 'vertical' ] = 1 + + # if we don't have enough space for terminal_maxwidth, then see if we have + # enough vertically for terminal_maxheight, in which case, + # that seems a better fit + term_horiz_max = ( settings.Int( 'sidebar_width' ) + + 1 + 2 + 3 + + settings.Int( 'code_minwidth' ) + + 1 + settings.Int( 'terminal_maxwidth' ) ) + term_vert_max = ( settings.Int( 'bottombar_height' ) + 1 + + settings.Int( 'code_minheight' ) + 1 + + settings.Int( 'terminal_minheight' ) ) + + if ( vim.options[ 'columns' ] < term_horiz_max and + vim.options[ 'lines' ] >= term_vert_max ): + # Looks like it, let's try that layout + term_options[ 'vertical' ] = 0 + + + else: + # vertical - we need enough space horizontally for the code+terminal, but we + # may fit better with code above terminal + term_options[ 'vertical' ] = 0 + + term_horiz_max = ( settings.Int( 'code_minwidth' ) + 3 + + settings.Int( 'terminal_maxwidth' ) + 1 ) + + if vim.options[ 'columns' ] > term_horiz_max: + term_options[ 'vertical' ] = 1 + + + if not window_for_start or not window_for_start.valid: + # TOOD: Where? Maybe we should just use botright vertical ... + window_for_start = vim.current.window + + if term.window is not None and term.window.valid: + assert term.buffer_number + window_for_start = term.window + if ( term.window.buffer.number == term.buffer_number + and int( utils.Call( 'vimspector#internal#{}term#IsFinished'.format( + api_prefix ), + term.buffer_number ) ) ): + term_options[ 'curwin' ] = 1 + else: + term_options[ 'vertical' ] = 0 + + buffer_number = None + terminal_window = None + with utils.LetCurrentWindow( window_for_start ): + # If we're making a vertical split from the code window, make it no more + # than 80 columns and no fewer than 10. Also try and keep the code window + # at least 82 columns + if term_options.get( 'curwin', 0 ): + pass + elif term_options[ 'vertical' ]: + term_options[ 'term_cols' ] = max( + min ( int( vim.eval( 'winwidth( 0 )' ) ) + - settings.Int( 'code_minwidth' ), + settings.Int( 'terminal_maxwidth' ) ), + settings.Int( 'terminal_minwidth' ) + ) + else: + term_options[ 'term_rows' ] = max( + min ( int( vim.eval( 'winheight( 0 )' ) ) + - settings.Int( 'code_minheight' ), + settings.Int( 'terminal_maxheight' ) ), + settings.Int( 'terminal_minheight' ) + ) + + + buffer_number = int( + utils.Call( + 'vimspector#internal#{}term#Start'.format( api_prefix ), + args, + term_options ) ) + terminal_window = vim.current.window + + if buffer_number is None or buffer_number <= 0: + # TODO: Do something better like reject the request? + raise ValueError( "Unable to start terminal" ) + + term.window = terminal_window + term.buffer_number = buffer_number + + vim.vars[ 'vimspector_session_windows' ][ 'terminal' ] = utils.WindowID( + term.window, + vim.current.tabpage ) + with utils.RestoreCursorPosition(): + with utils.RestoreCurrentWindow(): + with utils.RestoreCurrentBuffer( vim.current.window ): + vim.command( 'doautocmd User VimspectorTerminalOpened' ) + + return term diff --git a/python3/vimspector/utils.py b/python3/vimspector/utils.py index a3835b5..5f836fc 100644 --- a/python3/vimspector/utils.py +++ b/python3/vimspector/utils.py @@ -19,10 +19,18 @@ import os import contextlib import vim import json -import string +import functools +import subprocess +import shlex +import collections +import re +import typing + + +LOG_FILE = os.path.expanduser( os.path.join( '~', '.vimspector.log' ) ) + +_log_handler = logging.FileHandler( LOG_FILE, mode = 'w' ) -_log_handler = logging.FileHandler( os.path.expanduser( '~/.vimspector.log' ), - mode = 'w' ) _log_handler.setFormatter( logging.Formatter( '%(asctime)s - %(levelname)s - %(message)s' ) ) @@ -37,18 +45,38 @@ _logger = logging.getLogger( __name__ ) SetUpLogging( _logger ) -def BufferNumberForFile( file_name ): - return int( vim.eval( 'bufnr( "{0}", 1 )'.format( file_name ) ) ) +def BufferNumberForFile( file_name, create = True ): + return int( vim.eval( "bufnr( '{0}', {1} )".format( + Escape( file_name ), + int( create ) ) ) ) def BufferForFile( file_name ): return vim.buffers[ BufferNumberForFile( file_name ) ] +def BufferExists( file_name ): + return bool( int ( vim.eval( f"bufexists( '{ Escape( file_name ) }' )" ) ) ) + + +def NewEmptyBuffer(): + bufnr = int( vim.eval( 'bufadd("")' ) ) + Call( 'bufload', bufnr ) + return vim.buffers[ bufnr ] + + +def WindowForBuffer( buf ): + for w in vim.current.tabpage.windows: + if w.buffer == buf: + return w + + return None + + def OpenFileInCurrentWindow( file_name ): buffer_number = BufferNumberForFile( file_name ) try: - vim.command( 'bu {0}'.format( buffer_number ) ) + vim.current.buffer = vim.buffers[ buffer_number ] except vim.error as e: if 'E325' not in str( e ): raise @@ -56,36 +84,50 @@ def OpenFileInCurrentWindow( file_name ): return vim.buffers[ buffer_number ] -def SetUpCommandBuffer( cmd, name ): - bufs = vim.bindeval( - 'vimspector#internal#job#StartCommandWithLog( {}, "{}" )'.format( - json.dumps( cmd ), - name ) ) +COMMAND_HANDLERS = {} - if bufs is None: + +def OnCommandWithLogComplete( name, exit_code ): + cb = COMMAND_HANDLERS.get( name ) + if cb: + cb( exit_code ) + + +def SetUpCommandBuffer( cmd, name, api_prefix, completion_handler = None ): + COMMAND_HANDLERS[ name ] = completion_handler + + buf = Call( f'vimspector#internal#{api_prefix}job#StartCommandWithLog', + cmd, + name ) + + if buf is None: raise RuntimeError( "Unable to start job {}: {}".format( cmd, name ) ) - elif not all( b > 0 for b in bufs ): + elif int( buf ) <= 0: raise RuntimeError( "Unable to get all streams for job {}: {}".format( name, cmd ) ) - return [ vim.buffers[ b ] for b in bufs ] + return vim.buffers[ int( buf ) ] -def CleanUpCommand( name ): - return vim.eval( 'vimspector#internal#job#CleanUpCommand( "{}" )'.format( +def CleanUpCommand( name, api_prefix ): + return vim.eval( 'vimspector#internal#{}job#CleanUpCommand( "{}" )'.format( + api_prefix, name ) ) +def CleanUpHiddenBuffer( buf ): + try: + vim.command( 'bdelete! {}'.format( buf.number ) ) + except vim.error as e: + # FIXME: For now just ignore the "no buffers were deleted" error + if 'E516' not in str( e ): + raise + + def SetUpScratchBuffer( buf, name ): - buf.options[ 'buftype' ] = 'nofile' - buf.options[ 'swapfile' ] = False - buf.options[ 'modifiable' ] = False - buf.options[ 'modified' ] = False - buf.options[ 'readonly' ] = True - buf.options[ 'buflisted' ] = False + SetUpHiddenBuffer( buf, name ) buf.options[ 'bufhidden' ] = 'wipe' - buf.name = name def SetUpHiddenBuffer( buf, name ): @@ -99,10 +141,10 @@ def SetUpHiddenBuffer( buf, name ): buf.name = name -def SetUpPromptBuffer( buf, name, prompt, callback, hidden=False ): +def SetUpPromptBuffer( buf, name, prompt, callback, omnifunc ): # This feature is _super_ new, so only enable when available - if not int( vim.eval( "exists( '*prompt_setprompt' )" ) ): - return SetUpScratchBuffer( buf, name ) + if not Exists( '*prompt_setprompt' ): + return SetUpHiddenBuffer( buf, name ) buf.options[ 'buftype' ] = 'prompt' buf.options[ 'swapfile' ] = False @@ -110,7 +152,9 @@ def SetUpPromptBuffer( buf, name, prompt, callback, hidden=False ): buf.options[ 'modified' ] = False buf.options[ 'readonly' ] = False buf.options[ 'buflisted' ] = False - buf.options[ 'bufhidden' ] = 'wipe' if not hidden else 'hide' + buf.options[ 'bufhidden' ] = 'hide' + buf.options[ 'textwidth' ] = 0 + buf.options[ 'omnifunc' ] = omnifunc buf.name = name vim.eval( "prompt_setprompt( {0}, '{1}' )".format( buf.number, @@ -119,6 +163,20 @@ def SetUpPromptBuffer( buf, name, prompt, callback, hidden=False ): buf.number, Escape( callback ) ) ) + # This serves a few purposes, mainly to ensure that completion systems have + # something to work with. In particular it makes YCM use its identifier engine + # and you can config ycm to trigger semantic (annoyingly, synchronously) using + # some let g:ycm_auto_trggier + Call( 'setbufvar', buf.number, '&filetype', 'VimspectorPrompt' ) + + +def SetUpUIWindow( win ): + win.options[ 'wrap' ] = False + win.options[ 'number' ] = False + win.options[ 'relativenumber' ] = False + win.options[ 'signcolumn' ] = 'no' + win.options[ 'spell' ] = False + win.options[ 'list' ] = False @contextlib.contextmanager @@ -155,20 +213,40 @@ def RestoreCurrentWindow(): try: yield finally: - vim.current.tabpage = old_tabpage - vim.current.window = old_window + if old_tabpage.valid and old_window.valid: + vim.current.tabpage = old_tabpage + vim.current.window = old_window @contextlib.contextmanager def RestoreCurrentBuffer( window ): - # TODO: Don't trigger autoccommands when shifting buffers old_buffer = window.buffer try: yield finally: - with RestoreCurrentWindow(): - vim.current.window = window - vim.current.buffer = old_buffer + if window.valid: + with RestoreCurrentWindow(): + vim.current.window = window + vim.current.buffer = old_buffer + + +@contextlib.contextmanager +def AnyWindowForBuffer( buf ): + # Only checks the current tab page, which is what we want + current_win = WindowForBuffer( buf ) + if current_win is not None: + with LetCurrentWindow( current_win ): + yield + else: + with LetCurrentBuffer( buf ): + yield + + +@contextlib.contextmanager +def LetCurrentTabpage( tabpage ): + with RestoreCurrentWindow(): + vim.current.tabpage = tabpage + yield @contextlib.contextmanager @@ -178,6 +256,14 @@ def LetCurrentWindow( window ): yield +@contextlib.contextmanager +def LetCurrentBuffer( buf ): + with RestoreCursorPosition(): + with RestoreCurrentBuffer( vim.current.window ): + vim.current.buffer = buf + yield + + def JumpToWindow( window ): vim.current.tabpage = window.tabpage vim.current.window = window @@ -207,8 +293,12 @@ def TemporaryVimOption( opt, value ): vim.options[ opt ] = old_value -def PathToConfigFile( file_name ): - p = os.getcwd() +def PathToConfigFile( file_name, from_directory = None ): + if not from_directory: + p = os.getcwd() + else: + p = os.path.abspath( os.path.realpath( from_directory ) ) + while True: candidate = os.path.join( p, file_name ) if os.path.exists( candidate ): @@ -224,16 +314,21 @@ def Escape( msg ): return msg.replace( "'", "''" ) -def UserMessage( msg, persist=False ): +def UserMessage( msg, persist=False, error=False ): if persist: _logger.warning( 'User Msg: ' + msg ) else: _logger.info( 'User Msg: ' + msg ) - vim.command( 'redraw' ) cmd = 'echom' if persist else 'echo' - for line in msg.split( '\n' ): - vim.command( "{0} '{1}'".format( cmd, Escape( line ) ) ) + vim.command( 'redraw' ) + try: + if error: + vim.command( "echohl WarningMsg" ) + for line in msg.split( '\n' ): + vim.command( "{0} '{1}'".format( cmd, Escape( line ) ) ) + finally: + vim.command( 'echohl None' ) if error else None vim.command( 'redraw' ) @@ -257,25 +352,69 @@ def SelectFromList( prompt, options ): if selection < 0 or selection >= len( options ): return None return options[ selection ] - except KeyboardInterrupt: + except ( KeyboardInterrupt, vim.error ): return None -def AskForInput( prompt, default_value = None ): +def AskForInput( prompt, default_value = None, completion = None ): if default_value is None: - default_option = '' - else: - default_option = ", '{}'".format( Escape( default_value ) ) + default_value = '' + + args = [ prompt, default_value ] + + if completion is not None: + if completion == 'expr': + args.append( 'custom,vimspector#CompleteExpr' ) + else: + args.append( completion ) with InputSave(): try: - return vim.eval( "input( '{}' {} )".format( Escape( prompt ), - default_option ) ) - except KeyboardInterrupt: - return '' + return Call( 'input', *args ) + except ( KeyboardInterrupt, vim.error ): + return None + + +CONFIRM = {} +CONFIRM_ID = 0 + + +def ConfirmCallback( confirm_id, result ): + try: + handler = CONFIRM.pop( confirm_id ) + except KeyError: + UserMessage( f"Internal error: unexpected callback id { confirm_id }", + persist = True, + error = True ) + return + + handler( result ) + + +def Confirm( api_prefix, + prompt, + handler, + default_value = 2, + options: list = None, + keys: list = None ): + if not options: + options = [ '(Y)es', '(N)o' ] + if not keys: + keys = [ 'y', 'n' ] + + global CONFIRM_ID + CONFIRM_ID += 1 + CONFIRM[ CONFIRM_ID ] = handler + Call( f'vimspector#internal#{ api_prefix }popup#Confirm', + CONFIRM_ID, + prompt, + options, + default_value, + keys ) def AppendToBuffer( buf, line_or_lines, modified=False ): + line = 1 try: # After clearing the buffer (using buf[:] = None) there is always a single # empty line in the buffer object and no "is empty" method. @@ -302,8 +441,10 @@ def AppendToBuffer( buf, line_or_lines, modified=False ): -def ClearBuffer( buf ): +def ClearBuffer( buf, modified = False ): buf[ : ] = None + if not modified: + buf.options[ 'modified' ] = False def SetBufferContents( buf, lines, modified=False ): @@ -311,7 +452,7 @@ def SetBufferContents( buf, lines, modified=False ): if not isinstance( lines, list ): lines = lines.splitlines() - buf[:] = lines + buf[ : ] = lines finally: buf.options[ 'modified' ] = modified @@ -320,77 +461,195 @@ def IsCurrent( window, buf ): return vim.current.window == window and vim.current.window.buffer == buf -# TODO: Should we just run the substitution on the whole JSON string instead? -# That woul dallow expansion in bool and number values, such as ports etc. ? -def ExpandReferencesInDict( obj, mapping, user_choices ): - def expand_refs_in_string( orig_s ): - s = os.path.expanduser( orig_s ) - s = os.path.expandvars( s ) +def ExpandReferencesInObject( obj, mapping, calculus, user_choices ): + if isinstance( obj, dict ): + ExpandReferencesInDict( obj, mapping, calculus, user_choices ) + elif isinstance( obj, list ): + j_offset = 0 + obj_copy = list( obj ) - # Parse any variables passed in in mapping, and ask for any that weren't, - # storing the result in mapping - bug_catcher = 0 - while bug_catcher < 100: - ++bug_catcher + for i, _ in enumerate( obj_copy ): + j = i + j_offset + if ( isinstance( obj_copy[ i ], str ) and + len( obj_copy[ i ] ) > 2 and + obj_copy[ i ][ 0:2 ] == '*$' ): + # *${something} - expand list in place + value = ExpandReferencesInString( obj_copy[ i ][ 1: ], + mapping, + calculus, + user_choices ) + obj.pop( j ) + j_offset -= 1 + for opt_index, opt in enumerate( shlex.split( value ) ): + obj.insert( j + opt_index, opt ) + j_offset += 1 + else: + obj[ j ] = ExpandReferencesInObject( obj_copy[ i ], + mapping, + calculus, + user_choices ) + elif isinstance( obj, str ): + obj = ExpandReferencesInString( obj, mapping, calculus, user_choices ) + + return obj + + +# Based on the python standard library string.Template().substitue, enhanced to +# add ${name:default} parsing, and to remove the unnecessary generality. +VAR_MATCH = re.compile( + r""" + \$(?: # A dollar, followed by... + (?P\$) | # Another doller = escaped + (?P[_a-z][_a-z0-9]*) | # or An identifier - named param + {(?P[_a-z][_a-z0-9]*)} | # or An {identifier} - braced param + {(?P # or An {id:default} - default param, as + (?P[_a-z][_a-z0-9]*) # an ID + : # then a colon + (?P(?:\\}|[^}])*) # then anything up to }, or a \} + )} | # then a } + (?P) # or Something else - invalid + ) + """, + re.IGNORECASE | re.VERBOSE ) + + +class MissingSubstitution( Exception ): + def __init__( self, name, default_value = None ): + self.name = name + self.default_value = default_value + + +def _Substitute( template, mapping ): + def convert( mo ): + # Check the most common path first. + named = mo.group( 'named' ) or mo.group( 'braced' ) + if named is not None: + if named not in mapping: + raise MissingSubstitution( named ) + return str( mapping[ named ] ) + + if mo.group( 'escaped' ) is not None: + return '$' + + if mo.group( 'braceddefault' ) is not None: + named = mo.group( 'defname' ) + if named not in mapping: + raise MissingSubstitution( + named, + mo.group( 'default' ).replace( '\\}', '}' ) ) + return str( mapping[ named ] ) + + if mo.group( 'invalid' ) is not None: + raise ValueError( f"Invalid placeholder in string { template }" ) + + raise ValueError( 'Unrecognized named group in pattern', VAR_MATCH ) + + return VAR_MATCH.sub( convert, template ) + + +def ExpandReferencesInString( orig_s, + mapping, + calculus, + user_choices ): + s = os.path.expanduser( orig_s ) + s = os.path.expandvars( s ) + + # Parse any variables passed in in mapping, and ask for any that weren't, + # storing the result in mapping + bug_catcher = 0 + while bug_catcher < 100: + ++bug_catcher + + try: + s = _Substitute( s, mapping ) + break + except MissingSubstitution as e: + key = e.name + + if key in calculus: + mapping[ key ] = calculus[ key ]() + else: + default_value = user_choices.get( key ) + # Allow _one_ level of additional substitution. This allows a very real + # use case of "program": ${prgram:${file\\}} + if default_value is None and e.default_value is not None: + try: + default_value = _Substitute( e.default_value, mapping ) + except MissingSubstitution as e2: + if e2.name in calculus: + default_value = calculus[ e2.name ]() + else: + default_value = e.default_value - try: - s = string.Template( s ).substitute( mapping ) - break - except KeyError as e: - # HACK: This is seemingly the only way to get the key. str( e ) returns - # the key surrounded by '' for unknowable reasons. - key = e.args[ 0 ] - default_value = user_choices.get( key, None ) mapping[ key ] = AskForInput( 'Enter value for {}: '.format( key ), default_value ) + + if mapping[ key ] is None: + raise KeyboardInterrupt + user_choices[ key ] = mapping[ key ] _logger.debug( "Value for %s not set in %s (from %s): set to %s", key, s, orig_s, mapping[ key ] ) - except ValueError as e: - UserMessage( 'Invalid $ in string {}: {}'.format( s, e ), - persist = True ) - break + except ValueError as e: + UserMessage( 'Invalid $ in string {}: {}'.format( s, e ), + persist = True ) + break - return s - - def expand_refs_in_object( obj ): - if isinstance( obj, dict ): - ExpandReferencesInDict( obj, mapping, user_choices ) - elif isinstance( obj, list ): - for i, _ in enumerate( obj ): - # FIXME: We are assuming that it is a list of string, but could be a - # list of list of a list of dict, etc. - obj[ i ] = expand_refs_in_object( obj[ i ] ) - elif isinstance( obj, str ): - obj = expand_refs_in_string( obj ) - - return obj - - for k in obj.keys(): - obj[ k ] = expand_refs_in_object( obj[ k ] ) + return s -def ParseVariables( variables_list, mapping, user_choices ): +def CoerceType( mapping: typing.Dict[ str, typing.Any ], key: str ): + DICT_TYPES = { + 'json': json.loads, + 's': str + } + + parts = key.split( '#' ) + if len( parts ) > 1 and parts[ -1 ] in DICT_TYPES.keys(): + value = mapping.pop( key ) + + new_type = parts[ -1 ] + key = '#'.join( parts[ 0 : -1 ] ) + + mapping[ key ] = DICT_TYPES[ new_type ]( value ) + + +# TODO: Should we just run the substitution on the whole JSON string instead? +# That woul dallow expansion in bool and number values, such as ports etc. ? +def ExpandReferencesInDict( obj, mapping, calculus, user_choices ): + for k in list( obj.keys() ): + obj[ k ] = ExpandReferencesInObject( obj[ k ], + mapping, + calculus, + user_choices ) + CoerceType( obj, k ) + + +def ParseVariables( variables_list, + mapping, + calculus, + user_choices ): new_variables = {} new_mapping = mapping.copy() if not isinstance( variables_list, list ): variables_list = [ variables_list ] + variables: typing.Dict[ str, typing.Any ] for variables in variables_list: new_mapping.update( new_variables ) - for n, v in variables.items(): + for n, v in list( variables.items() ): if isinstance( v, dict ): if 'shell' in v: - import subprocess - import shlex - new_v = v.copy() # Bit of a hack. Allows environment variables to be used. - ExpandReferencesInDict( new_v, new_mapping, user_choices ) + ExpandReferencesInDict( new_v, + new_mapping, + calculus, + user_choices ) env = os.environ.copy() env.update( new_v.get( 'env' ) or {} ) @@ -412,17 +671,29 @@ def ParseVariables( variables_list, mapping, user_choices ): raise ValueError( "Unsupported variable defn {}: Missing 'shell'".format( n ) ) else: - new_variables[ n ] = v + new_variables[ n ] = ExpandReferencesInObject( v, + mapping, + calculus, + user_choices ) + + CoerceType( new_variables, n ) return new_variables -def DisplayBaloon( is_term, display ): +def DisplayBalloon( is_term, display, is_hover = False ): if not is_term: + # To enable the Windows GUI to display the balloon correctly + # Refer https://github.com/vim/vim/issues/1512#issuecomment-492070685 display = '\n'.join( display ) - vim.eval( "balloon_show( {0} )".format( - json.dumps( display ) ) ) + created_win_id = int( vim.eval( + "vimspector#internal#balloon#CreateTooltip({}, {})".format( + int( is_hover ), json.dumps( display ) + ) + ) ) + + return created_win_id def GetBufferFilepath( buf ): @@ -430,3 +701,166 @@ def GetBufferFilepath( buf ): return '' return os.path.normpath( buf.name ) + + +def ToUnicode( b ): + if isinstance( b, bytes ): + return b.decode( 'utf-8' ) + return b + + +# Call a vimscript function with suplied arguments. +def Call( vimscript_function, *args ): + call = vimscript_function + '(' + for index, arg in enumerate( args ): + if index > 0: + call += ', ' + + arg_name = 'vimspector_internal_arg_{}'.format( index ) + vim.vars[ arg_name ] = arg + call += 'g:' + arg_name + + call += ')' + return vim.eval( call ) + + +MEMO = {} + + +def memoize( func ): + global MEMO + + @functools.wraps( func ) + def wrapper( *args, **kwargs ): + dct = MEMO.setdefault( func, {} ) + key = ( args, frozenset( kwargs.items() ) ) + try: + return dct[ key ] + except KeyError: + result = func( *args, **kwargs ) + dct[ key ] = result + return result + + return wrapper + + +@memoize +def Exists( expr ): + return int( vim.eval( f'exists( "{ expr }" )' ) ) + + +def SetSyntax( current_syntax, syntax, *args ): + if not syntax: + syntax = '' + + if current_syntax == syntax: + return syntax + + # We use set syn= because just setting vim.Buffer.options[ 'syntax' ] + # doesn't actually trigger the Syntax autocommand, and i'm not sure that + # 'doautocmd Syntax' is the right solution or not + for buf in args: + Call( 'setbufvar', buf.number, '&syntax', syntax ) + + return syntax + + +def GetBufferFiletypes( buf ): + ft = ToUnicode( vim.eval( f"getbufvar( {buf.number}, '&ft' )" ) ) + return ft.split( '.' ) + + +def GetVisualSelection( bufnr ): + start_line, start_col = vim.current.buffer.mark( "<" ) + end_line, end_col = vim.current.buffer.mark( ">" ) + + + # lines are 1 based, but columns are 0 based + # don't ask me why... + start_line -= 1 + end_line -= 1 + + lines = vim.buffers[ bufnr ][ start_line : end_line + 1 ] + # Do end first, in case it's on the same line as start (as doing start first + # would change the offset) + lines[ -1 ] = lines[ -1 ][ : end_col + 1 ] + lines[ 0 ] = lines[ 0 ][ start_col : ] + + _logger.debug( f'Visual selection: { lines } from ' + f'{ start_line }/{ start_col } -> { end_line }/{ end_col }' ) + + return lines + + +def DisplaySplash( api_prefix, splash, text ): + if splash: + return Call( f'vimspector#internal#{api_prefix}popup#UpdateSplash', + splash, + text ) + else: + return Call( f'vimspector#internal#{api_prefix}popup#DisplaySplash', + text ) + + +def HideSplash( api_prefix, splash ): + if splash: + Call( f'vimspector#internal#{api_prefix}popup#HideSplash', splash ) + + return None + + +def GetVimValue( vim_dict, name, default=None ): + + # FIXME: use 'encoding' ? + try: + value = vim_dict[ name ] + except ( KeyError, vim.error ): + return default + + if isinstance( value, bytes ): + return value.decode( 'utf-8' ) + return value + + +def GetVimList( vim_dict, name, default=None ): + try: + value = vim_dict[ name ] + except ( KeyError, vim.error ): + return default + + if not isinstance( value, collections.abc.Iterable ): + raise ValueError( f"Expected a list for { name }, but found " + f"{ type( value ) }" ) + + return [ i.decode( 'utf-8' ) if isinstance( i, bytes ) else i for i in value ] + + +def GetVimspectorBase(): + return GetVimValue( vim.vars, + 'vimspector_base_dir', + os.path.abspath( + os.path.join( os.path.dirname( __file__ ), + '..', + '..' ) ) ) + + +def GetUnusedLocalPort(): + import socket + sock = socket.socket() + # This tells the OS to give us any free port in the range [1024 - 65535] + sock.bind( ( '', 0 ) ) + port = sock.getsockname()[ 1 ] + sock.close() + return port + + +def WindowID( window, tab=None ): + if tab is None: + tab = window.tabpage + return int( Call( 'win_getid', window.number, tab.number ) ) + + +def UseWinBar(): + # Buggy neovim doesn't render correctly when the WinBar is defined: + # https://github.com/neovim/neovim/issues/12689 + return not int( Call( 'has', 'nvim' ) ) diff --git a/python3/vimspector/variables.py b/python3/vimspector/variables.py index d9882c8..8dcb493 100644 --- a/python3/vimspector/variables.py +++ b/python3/vimspector/variables.py @@ -13,57 +13,208 @@ # See the License for the specific language governing permissions and # limitations under the License. +import abc import vim import logging -from collections import namedtuple from functools import partial +import typing -from vimspector import utils +from vimspector import utils, settings -View = namedtuple( 'View', [ 'win', 'lines', 'draw' ] ) + +class Expandable: + EXPANDED_BY_USER = 2 + EXPANDED_BY_US = 1 + COLLAPSED_BY_USER = 0 + COLLAPSED_BY_DEFAULT = None + + """Base for anything which might contain a hierarchy of values represented by + a 'variablesReference' to be resolved by the 'variables' request. Records the + current state expanded/collapsed. Implementations just implement + VariablesReference to get the variables.""" + def __init__( self, container: 'Expandable' = None ): + self.variables: typing.List[ 'Variable' ] = None + self.container: Expandable = container + # None is Falsy and represents collapsed _by default_. WHen set to False, + # this means the user explicitly collapsed it. When True, the user expanded + # it (or we expanded it by default). + self.expanded: int = Expandable.COLLAPSED_BY_DEFAULT + + def IsExpanded( self ): + return bool( self.expanded ) + + def ShouldDrawDrillDown( self ): + return self.IsExpanded() and self.variables is not None + + def IsExpandable( self ): + return self.VariablesReference() > 0 + + def IsContained( self ): + return self.container is not None + + @abc.abstractmethod + def VariablesReference( self ): + assert False + + +class Scope( Expandable ): + """Holds an expandable scope (a DAP scope dict), with expand/collapse state""" + def __init__( self, scope: dict ): + super().__init__() + self.scope = scope + + def VariablesReference( self ): + return self.scope.get( 'variablesReference', 0 ) + + def Update( self, scope ): + self.scope = scope + + +class WatchResult( Expandable ): + """Holds the result of a Watch expression with expand/collapse.""" + def __init__( self, result: dict ): + super().__init__() + self.result = result + # A new watch result is marked as changed + self.changed = True + + def VariablesReference( self ): + return self.result.get( 'variablesReference', 0 ) + + def Update( self, result ): + self.changed = False + if self.result[ 'result' ] != result[ 'result' ]: + self.changed = True + self.result = result + + +class WatchFailure( WatchResult ): + def __init__( self, reason ): + super().__init__( { 'result': reason } ) + self.changed = True + + +class Variable( Expandable ): + """Holds one level of an expanded value tree. Also itself expandable.""" + def __init__( self, container: Expandable, variable: dict ): + super().__init__( container = container ) + self.variable = variable + # A new variable appearing is marked as changed + self.changed = True + + def VariablesReference( self ): + return self.variable.get( 'variablesReference', 0 ) + + def Update( self, variable ): + self.changed = False + if self.variable[ 'value' ] != variable[ 'value' ]: + self.changed = True + self.variable = variable + + +class Watch: + """Holds a user watch expression (DAP request) and the result (WatchResult)""" + def __init__( self, expression: dict ): + self.result: WatchResult + self.line = None + + self.expression = expression + self.result = None + + @staticmethod + def New( frame, expression, context ): + watch = { + 'expression': expression, + 'context': context, + } + if frame: + watch[ 'frameId' ] = frame[ 'id' ] + + return Watch( watch ) + + +class View: + lines: typing.Dict[ int, Expandable ] + draw: typing.Callable + syntax: str + + def __init__( self, win, lines, draw ): + self.lines = lines + self.draw = draw + self.syntax = None + if win is not None: + self.buf = win.buffer + utils.SetUpUIWindow( win ) + + +class BufView( View ): + def __init__( self, buf, lines, draw ): + super().__init__( None, lines, draw ) + self.buf = buf + + +def AddExpandMappings( mappings = None ): + if mappings is None: + mappings = settings.Dict( 'mappings' )[ 'variables' ] + for mapping in utils.GetVimList( mappings, 'expand_collapse' ): + vim.command( f'nnoremap { mapping } ' + ':call vimspector#ExpandVariable()' ) + + for mapping in utils.GetVimList( mappings, 'set_value' ): + vim.command( f'nnoremap { mapping } ' + ':call vimspector#SetVariableValue()' ) class VariablesView( object ): - def __init__( self, connection, variables_win, watches_win ): + def __init__( self, variables_win, watches_win ): self._logger = logging.getLogger( __name__ ) utils.SetUpLogging( self._logger ) + self._connection = None + self._current_syntax = '' + self._server_capabilities = None + + self._variable_eval: Scope = None + self._variable_eval_view: View = None + + mappings = settings.Dict( 'mappings' )[ 'variables' ] + + # Set up the "Variables" buffer in the variables_win + self._scopes: typing.List[ Scope ] = [] self._vars = View( variables_win, {}, self._DrawScopes ) + utils.SetUpHiddenBuffer( self._vars.buf, 'vimspector.Variables' ) + with utils.LetCurrentWindow( variables_win ): + if utils.UseWinBar(): + vim.command( 'nnoremenu 1.1 WinBar.Set ' + ':call vimspector#SetVariableValue()' ) + AddExpandMappings( mappings ) + + # Set up the "Watches" buffer in the watches_win (and create a WinBar in + # there) + self._watches: typing.List[ Watch ] = [] self._watch = View( watches_win, {}, self._DrawWatches ) - self._connection = connection - - # Allows us to hit to expand/collapse variables - with utils.LetCurrentWindow( self._vars.win ): - vim.command( - 'nnoremap :call vimspector#ExpandVariable()' ) - - # This is actually the tree (scopes are alwyas the root) - # it's just a list of DAP scope dicts, with one magic key (_variables) - # _variables is a list of DAP variable with the same magic key - # - # If _variables is present, then we have requested and should display the - # children. Otherwise, we haven't or shouldn't. - self._scopes = [] - - # This is similar to scopes, but the top level is an "expression" (request) - # containing a special '_result' key which is the response. The response - # structure con contain _variables and is handled identically to the scopes - # above. It also has a special _line key which is where we printed it (last) - self._watches = [] - - # Allows us to hit to expand/collapse variables - with utils.LetCurrentWindow( self._watch.win ): - vim.command( - 'nnoremap :call vimspector#ExpandVariable()' ) - vim.command( - 'nnoremap :call vimspector#DeleteWatch()' ) - - utils.SetUpScratchBuffer( self._vars.win.buffer, 'vimspector.Variables' ) - utils.SetUpPromptBuffer( self._watch.win.buffer, + utils.SetUpPromptBuffer( self._watch.buf, 'vimspector.Watches', 'Expression: ', - 'vimspector#AddWatchPrompt' ) + 'vimspector#AddWatchPrompt', + 'vimspector#OmniFuncWatch' ) + with utils.LetCurrentWindow( watches_win ): + AddExpandMappings( mappings ) + for mapping in utils.GetVimList( mappings, 'delete' ): + vim.command( + f'nnoremap { mapping } :call vimspector#DeleteWatch()' ) + if utils.UseWinBar(): + vim.command( 'nnoremenu 1.1 WinBar.New ' + ':call vimspector#AddWatch()' ) + vim.command( 'nnoremenu 1.2 WinBar.Expand/Collapse ' + ':call vimspector#ExpandVariable()' ) + vim.command( 'nnoremenu 1.3 WinBar.Delete ' + ':call vimspector#DeleteWatch()' ) + vim.command( 'nnoremenu 1.1 WinBar.Set ' + ':call vimspector#SetVariableValue()' ) + + # Set the (global!) balloon expr if supported has_balloon = int( vim.eval( "has( 'balloon_eval' )" ) ) has_balloon_term = int( vim.eval( "has( 'balloon_eval_term' )" ) ) @@ -73,7 +224,9 @@ class VariablesView( object ): 'balloonexpr': vim.options[ 'balloonexpr' ], 'balloondelay': vim.options[ 'balloondelay' ], } - vim.options[ 'balloonexpr' ] = 'vimspector#internal#balloon#BalloonExpr()' + vim.options[ 'balloonexpr' ] = ( "vimspector#internal#" + "balloon#HoverTooltip()" ) + vim.options[ 'balloondelay' ] = 250 if has_balloon: @@ -87,49 +240,76 @@ class VariablesView( object ): self._is_term = not bool( int( vim.eval( "has( 'gui_running' )" ) ) ) def Clear( self ): - with utils.ModifiableScratchBuffer( self._vars.win.buffer ): - utils.ClearBuffer( self._vars.win.buffer ) - with utils.ModifiableScratchBuffer( self._watch.win.buffer ): - utils.ClearBuffer( self._watch.win.buffer ) + with utils.ModifiableScratchBuffer( self._vars.buf ): + utils.ClearBuffer( self._vars.buf ) + with utils.ModifiableScratchBuffer( self._watch.buf ): + utils.ClearBuffer( self._watch.buf ) + self.ClearTooltip() + self._current_syntax = '' def ConnectionUp( self, connection ): self._connection = connection + def SetServerCapabilities( self, capabilities ): + self._server_capabilities = capabilities + def ConnectionClosed( self ): self.Clear() self._connection = None + self._server_capabilities = None def Reset( self ): + self._server_capabilities = None + for k, v in self._oldoptions.items(): vim.options[ k ] = v + utils.CleanUpHiddenBuffer( self._vars.buf ) + utils.CleanUpHiddenBuffer( self._watch.buf ) + self.ClearTooltip() + + def LoadScopes( self, frame ): def scopes_consumer( message ): - old_scopes = self._scopes - self._scopes = [] + new_scopes = [] + expanded_some_scope = False + for scope_body in message[ 'body' ][ 'scopes' ]: + # Find it in the scopes list + found = False + for index, s in enumerate( self._scopes ): + if s.scope[ 'name' ] == scope_body[ 'name' ]: + found = True + scope = s + break - for i, scope in enumerate( message[ 'body' ][ 'scopes' ] ): - if ( i < len( old_scopes ) and - old_scopes[ i ][ 'name' ] == scope[ 'name' ] ): - scope[ '_expanded' ] = old_scopes[ i ].get( '_expanded', False ) - scope[ '_old_variables' ] = old_scopes[ i ].get( '_variables', [] ) - elif not scope.get( 'expensive' ): - # Expand any non-expensive scope unless manually collapsed - scope[ '_expanded' ] = True + if not found: + scope = Scope( scope_body ) else: - scope[ '_expanded' ] = False + scope.Update( scope_body ) - self._scopes.append( scope ) - if scope[ '_expanded' ]: + new_scopes.append( scope ) + + # Expand the first non-expensive scope which is not manually collapsed + if ( not expanded_some_scope + and not scope.scope.get( 'expensive' ) + and scope.expanded is not Expandable.COLLAPSED_BY_USER ): + scope.expanded = Expandable.EXPANDED_BY_US + expanded_some_scope = True + elif ( expanded_some_scope and scope.expanded is + Expandable.EXPANDED_BY_US ): + scope.expanded = Expandable.COLLAPSED_BY_DEFAULT + + if scope.IsExpanded(): self._connection.DoRequest( partial( self._ConsumeVariables, self._DrawScopes, scope ), { 'command': 'variables', 'arguments': { - 'variablesReference': scope[ 'variablesReference' ] + 'variablesReference': scope.VariablesReference(), }, } ) + self._scopes = new_scopes self._DrawScopes() self._connection.DoRequest( scopes_consumer, { @@ -139,237 +319,413 @@ class VariablesView( object ): }, } ) + def _DrawBalloonEval( self ): + watch = self._variable_eval + view = self._variable_eval_view + + with utils.RestoreCursorPosition(): + with utils.ModifiableScratchBuffer( view.buf ): + utils.ClearBuffer( view.buf ) + view.syntax = utils.SetSyntax( view.syntax, + self._current_syntax, + view.buf ) + + self._DrawWatchResult( view, + 0, + watch, + is_short = True ) + + vim.eval( "vimspector#internal#balloon#ResizeTooltip()" ) + + def ClearTooltip( self ): + # This will actually end up calling CleanUpTooltip via the popup close + # callback + vim.eval( 'vimspector#internal#balloon#Close()' ) + + def CleanUpTooltip( self ) : + # remove reference to old tooltip window + self._variable_eval_view = None + vim.vars[ 'vimspector_session_windows' ][ 'eval' ] = None + + def VariableEval( self, frame, expression, is_hover ): + """Callback to display variable under cursor `:h ballonexpr`""" + if not self._connection: + return '' + + def handler( message ): + + watch = self._variable_eval + if watch.result is None: + watch.result = WatchResult( message[ 'body' ] ) + else: + watch.result.Update( message[ 'body' ] ) + + popup_win_id = utils.DisplayBalloon( self._is_term, [], is_hover ) + # record the global eval window id + vim.vars[ 'vimspector_session_windows' ][ 'eval' ] = int( popup_win_id ) + popup_bufnr = int( vim.eval( "winbufnr({})".format( popup_win_id ) ) ) + + # We don't need to do any UI window setup here, as it's already done as + # part of the popup creation, so just pass the buffer to the View instance + self._variable_eval_view = BufView( + vim.buffers[ popup_bufnr ], + {}, + self._DrawBalloonEval + ) + + if watch.result.IsExpandable(): + # Always expand the first level + watch.result.expanded = Expandable.EXPANDED_BY_US + + if watch.result.IsExpanded(): + self._connection.DoRequest( partial( self._ConsumeVariables, + self._variable_eval_view.draw, + watch.result ), { + 'command': 'variables', + 'arguments': { + 'variablesReference': watch.result.VariablesReference(), + }, + } ) + + self._DrawBalloonEval() + + def failure_handler( reason, message ): + display = [ reason ] + float_win_id = utils.DisplayBalloon( self._is_term, display, is_hover ) + # record the global eval window id + vim.vars[ 'vimspector_session_windows' ][ 'eval' ] = int( float_win_id ) + + self._variable_eval = Watch.New( frame, + expression, + 'hover' ) + + # Send async request + self._connection.DoRequest( handler, { + 'command': 'evaluate', + 'arguments': self._variable_eval.expression, + }, failure_handler ) + + # Return working (meanwhile) + return '' + def AddWatch( self, frame, expression ): - watch = { - 'expression': expression, - 'frameId': frame[ 'id' ], - 'context': 'watch', - } - self._watches.append( watch ) + self._watches.append( Watch.New( frame, expression, 'watch' ) ) self.EvaluateWatches() def DeleteWatch( self ): - if vim.current.window != self._watch.win: - utils.UserMessage( 'Not a watch window' ) + if vim.current.buffer != self._watch.buf: + utils.UserMessage( 'Not a watch buffer' ) return current_line = vim.current.window.cursor[ 0 ] + best_index = -1 for index, watch in enumerate( self._watches ): - if '_line' in watch and watch[ '_line' ] == current_line: - del self._watches[ index ] - utils.UserMessage( 'Deleted' ) - self._DrawWatches() - return + if ( watch.line is not None + and watch.line <= current_line + and watch.line > best_index ): + best_index = index + + if best_index >= 0: + del self._watches[ best_index ] + utils.UserMessage( 'Deleted' ) + self._DrawWatches() + return utils.UserMessage( 'No watch found' ) def EvaluateWatches( self ): for watch in self._watches: - self._connection.DoRequest( partial( self._UpdateWatchExpression, - watch ), { - 'command': 'evaluate', - 'arguments': watch, - } ) + self._connection.DoRequest( + partial( self._UpdateWatchExpression, watch ), + { + 'command': 'evaluate', + 'arguments': watch.expression, + }, + failure_handler = lambda reason, msg, watch=watch: + self._WatchExpressionFailed( reason, watch ) ) - def _UpdateWatchExpression( self, watch, message ): - old_result = None - if '_result' in watch: - old_result = watch[ '_result' ] + def _UpdateWatchExpression( self, watch: Watch, message: dict ): + if watch.result is not None: + watch.result.Update( message[ 'body' ] ) + else: + watch.result = WatchResult( message[ 'body' ] ) - result = message[ 'body' ] - watch[ '_result' ] = result - - if old_result: - if '_expanded' in old_result: - result[ '_expanded' ] = old_result[ '_expanded' ] - result[ '_old_variables' ] = old_result.get( '_variables', [] ) - - if ( result.get( 'variablesReference', 0 ) > 0 and - result.get( '_expanded', False ) ): + if ( watch.result.IsExpandable() and + watch.result.IsExpanded() ): self._connection.DoRequest( partial( self._ConsumeVariables, self._watch.draw, - result ), { + watch.result ), { 'command': 'variables', 'arguments': { - 'variablesReference': result[ 'variablesReference' ] + 'variablesReference': watch.result.VariablesReference(), }, } ) self._DrawWatches() - def ExpandVariable( self ): - if vim.current.window == self._vars.win: + def _WatchExpressionFailed( self, reason: str, watch: Watch ): + if watch.result is not None: + # We already have a result for this watch. Wut ? + return + + watch.result = WatchFailure( reason ) + self._DrawWatches() + + def _GetVariable( self, buf = None, line_num = None ): + none = ( None, None ) + + if buf is None: + buf = vim.current.buffer + + if line_num is None: + line_num = vim.current.window.cursor[ 0 ] + + if buf == self._vars.buf: view = self._vars - elif vim.current.window == self._watch.win: + elif buf == self._watch.buf: view = self._watch + elif ( self._variable_eval_view is not None + and buf == self._variable_eval_view.buf ): + view = self._variable_eval_view else: + return none + + if line_num not in view.lines: + return none + + return view.lines[ line_num ], view + + def ExpandVariable( self, buf = None, line_num = None ): + variable, view = self._GetVariable( buf, line_num ) + if variable is None: return - current_line = vim.current.window.cursor[ 0 ] - if current_line not in view.lines: - return - - variable = view.lines[ current_line ] - - if '_variables' in variable: + if variable.IsExpanded(): # Collapse - del variable[ '_variables' ] - variable[ '_expanded' ] = False + variable.expanded = Expandable.COLLAPSED_BY_USER view.draw() return - if variable.get( 'variablesReference', 0 ) <= 0: + if not variable.IsExpandable(): return - variable[ '_expanded' ] = True + variable.expanded = Expandable.EXPANDED_BY_USER self._connection.DoRequest( partial( self._ConsumeVariables, view.draw, variable ), { 'command': 'variables', 'arguments': { - 'variablesReference': variable[ 'variablesReference' ] + 'variablesReference': variable.VariablesReference() }, } ) - def _DrawVariables( self, view, variables, indent ): + def SetVariableValue( self, new_value = None, buf = None, line_num = None ): + variable: Variable + view: View + + if not self._server_capabilities.get( 'supportsSetVariable' ): + return + + variable, view = self._GetVariable( buf, line_num ) + if variable is None: + return + + if not variable.IsContained(): + return + + if new_value is None: + new_value = utils.AskForInput( 'New Value: ', + variable.variable.get( 'value', '' ), + completion = 'expr' ) + + if new_value is None: + return + + + def handler( message ): + # Annoyingly the response to setVariable request doesn't return a + # Variable, but some part of it, so take a copy of the existing Variable + # dict and update it, then call its update method with the updated copy. + new_variable = dict( variable.variable ) + new_variable.update( message[ 'body' ] ) + + # Clear any existing known children (FIXME: Is this the right thing to do) + variable.variables = None + + # If the variable is expanded, re-request its children + if variable.IsExpanded(): + self._connection.DoRequest( partial( self._ConsumeVariables, + view.draw, + variable ), { + 'command': 'variables', + 'arguments': { + 'variablesReference': variable.VariablesReference() + }, + } ) + + variable.Update( new_variable ) + view.draw() + + def failure_handler( reason, message ): + utils.UserMessage( f'Cannot set value: { reason }', error = True ) + + self._connection.DoRequest( handler, { + 'command': 'setVariable', + 'arguments': { + 'variablesReference': variable.container.VariablesReference(), + 'name': variable.variable[ 'name' ], + 'value': new_value + }, + }, failure_handler = failure_handler ) + + + + def _DrawVariables( self, view, variables, indent, is_short = False ): + assert indent > 0 for variable in variables: + text = '' + if is_short: + text = '{indent}{icon} {name}: {value}'.format( + # We borrow 1 space of indent to draw the change marker + indent = ' ' * ( indent - 1 ), + icon = '+' if ( variable.IsExpandable() + and not variable.IsExpanded() ) else '-', + name = variable.variable.get( 'name', '' ), + value = variable.variable.get( 'value', '' ) + ) + else: + text = '{indent}{marker}{icon} {name} ({type_}): {value}'.format( + # We borrow 1 space of indent to draw the change marker + indent = ' ' * ( indent - 1 ), + marker = '*' if variable.changed else ' ', + icon = '+' if ( variable.IsExpandable() + and not variable.IsExpanded() ) else '-', + name = variable.variable.get( 'name', '' ), + type_ = variable.variable.get( 'type', '' ), + value = variable.variable.get( 'value', '' ) + ) + line = utils.AppendToBuffer( - view.win.buffer, - '{indent}{icon} {name} ({type_}): {value}'.format( - indent = ' ' * indent, - icon = '+' if ( variable.get( 'variablesReference', 0 ) > 0 and - '_variables' not in variable ) else '-', - name = variable[ 'name' ], - type_ = variable.get( 'type', '' ), - value = variable.get( 'value', '' ) ).split( '\n' ) ) + view.buf, + text.split( '\n' ) + ) + view.lines[ line ] = variable - if '_variables' in variable: - self._DrawVariables( view, variable[ '_variables' ], indent + 2 ) + if variable.ShouldDrawDrillDown(): + self._DrawVariables( view, variable.variables, indent + 2, is_short ) def _DrawScopes( self ): # FIXME: The drawing is dumb and draws from scratch every time. This is # simple and works and makes sure the line-map is always correct. - # However it is really inefficient, and makes it so that expanded results - # are collapsed on every step. + # However it is pretty inefficient. self._vars.lines.clear() with utils.RestoreCursorPosition(): - with utils.ModifiableScratchBuffer( self._vars.win.buffer ): - utils.ClearBuffer( self._vars.win.buffer ) + with utils.ModifiableScratchBuffer( self._vars.buf ): + utils.ClearBuffer( self._vars.buf ) for scope in self._scopes: self._DrawScope( 0, scope ) def _DrawWatches( self ): # FIXME: The drawing is dumb and draws from scratch every time. This is # simple and works and makes sure the line-map is always correct. - # However it is really inefficient, and makes it so that expanded results - # are collapsed on every step. + # However it is pretty inefficient. self._watch.lines.clear() with utils.RestoreCursorPosition(): - with utils.ModifiableScratchBuffer( self._watch.win.buffer ): - utils.ClearBuffer( self._watch.win.buffer ) - utils.AppendToBuffer( self._watch.win.buffer, 'Watches: ----' ) + with utils.ModifiableScratchBuffer( self._watch.buf ): + utils.ClearBuffer( self._watch.buf ) + utils.AppendToBuffer( self._watch.buf, 'Watches: ----' ) for watch in self._watches: - line = utils.AppendToBuffer( self._watch.win.buffer, - 'Expression: ' + watch[ 'expression' ] ) - watch[ '_line' ] = line - self._DrawWatchResult( 2, watch ) + line = utils.AppendToBuffer( self._watch.buf, + 'Expression: ' + + watch.expression[ 'expression' ] ) + watch.line = line + self._DrawWatchResult( self._watch, 2, watch ) def _DrawScope( self, indent, scope ): - icon = '+' if ( scope.get( 'variablesReference', 0 ) > 0 and - '_variables' not in scope ) else '-' + icon = '+' if scope.IsExpandable() and not scope.IsExpanded() else '-' - line = utils.AppendToBuffer( self._vars.win.buffer, - '{0}{1} Scope: {2}'.format( ' ' * indent, - icon, - scope[ 'name' ] ) ) + line = utils.AppendToBuffer( self._vars.buf, + '{0}{1} Scope: {2}'.format( + ' ' * indent, + icon, + scope.scope[ 'name' ] ) ) self._vars.lines[ line ] = scope - if '_variables' in scope: + if scope.ShouldDrawDrillDown(): indent += 2 - self._DrawVariables( self._vars, scope[ '_variables' ], indent ) + self._DrawVariables( self._vars, scope.variables, indent ) - def _DrawWatchResult( self, indent, watch ): - if '_result' not in watch: + def _DrawWatchResult( self, view, indent, watch, is_short = False ): + if not watch.result: return - result = watch[ '_result' ] + assert is_short or indent > 0 - icon = '+' if ( result.get( 'variablesReference', 0 ) > 0 and - '_variables' not in result ) else '-' + if is_short: + # The first result is always expanded in a hover (short format) + icon = '' + marker = '' + leader = '' + else: + icon = '+' if ( watch.result.IsExpandable() and + not watch.result.IsExpanded() ) else '-' + marker = '*' if watch.result.changed else ' ' + leader = ' Result: ' - result_str = result[ 'result' ] - if result_str is None: - result_str = 'null' + line = '{indent}{marker}{icon}{leader}{result}'.format( + # We borrow 1 space of indent to draw the change marker + indent = ' ' * ( indent - 1 ), + marker = marker, + icon = icon, + leader = leader, + result = watch.result.result.get( 'result', '' ) ) - line = '{0}{1} Result: {2} '.format( ' ' * indent, icon, result_str ) - line = utils.AppendToBuffer( self._watch.win.buffer, line.split( '\n' ) ) - self._watch.lines[ line ] = result + line = utils.AppendToBuffer( view.buf, line.split( '\n' ) ) + view.lines[ line ] = watch.result - if '_variables' in result: - indent = 4 - self._DrawVariables( self._watch, result[ '_variables' ], indent ) + if watch.result.ShouldDrawDrillDown(): + self._DrawVariables( view, watch.result.variables, indent + 2, is_short ) def _ConsumeVariables( self, draw, parent, message ): - for variable in message[ 'body' ][ 'variables' ]: - if '_variables' not in parent: - parent[ '_variables' ] = [] - - parent[ '_variables' ].append( variable ) - - # If the variable was previously expanded, expand it again - for index, v in enumerate( parent.get( '_old_variables', [] ) ): - if v[ 'name' ] == variable[ 'name' ]: - if ( v.get( '_expanded', False ) and - variable.get( 'variablesReference', 0 ) > 0 ): - - variable[ '_expanded' ] = True - variable[ '_old_variables' ] = v.get( '_variables', [] ) - - self._connection.DoRequest( partial( self._ConsumeVariables, - draw, - variable ), { - 'command': 'variables', - 'arguments': { - 'variablesReference': variable[ 'variablesReference' ] - }, - } ) + new_variables = [] + for variable_body in message[ 'body' ][ 'variables' ]: + if parent.variables is None: + parent.variables = [] + # Find the variable in parent + found = False + for index, v in enumerate( parent.variables ): + if v.variable[ 'name' ] == variable_body[ 'name' ]: + variable = v + found = True break + if not found: + variable = Variable( parent, variable_body ) + else: + variable.Update( variable_body ) - if '_old_variables' in parent: - del parent[ '_old_variables' ] + new_variables.append( variable ) + + if variable.IsExpandable() and variable.IsExpanded(): + self._connection.DoRequest( partial( self._ConsumeVariables, + draw, + variable ), { + 'command': 'variables', + 'arguments': { + 'variablesReference': variable.VariablesReference() + }, + } ) + + parent.variables = new_variables draw() - def ShowBalloon( self, frame, expression ): - if not self._connection: - return - - def handler( message ): - # TODO: this result count be expandable, but we have no way to allow the - # user to interact with the balloon to expand it. - body = message[ 'body' ] - result = body[ 'result' ] - if result is None: - result = 'null' - display = [ - 'Type: ' + body.get( 'type', '' ), - 'Value: ' + result - ] - utils.DisplayBaloon( self._is_term, display ) - - def failure_handler( reason, message ): - display = [ reason ] - utils.DisplayBaloon( self._is_term, display ) - - - self._connection.DoRequest( handler, { - 'command': 'evaluate', - 'arguments': { - 'expression': expression, - 'frameId': frame[ 'id' ], - 'context': 'hover', - } - }, failure_handler ) + def SetSyntax( self, syntax ): + # TODO: Switch to View.syntax + self._current_syntax = utils.SetSyntax( self._current_syntax, + syntax, + self._vars.buf, + self._watch.buf ) +# vim: sw=2 diff --git a/python3/vimspector/vendor/json_minify.py b/python3/vimspector/vendor/json_minify.py new file mode 100644 index 0000000..6b78901 --- /dev/null +++ b/python3/vimspector/vendor/json_minify.py @@ -0,0 +1,97 @@ +# Copyright © 2020 Kyle Simpson +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the "Software"), +# to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, +# and/or sell copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE +# OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# + +"""A port of the `JSON-minify` utility to the Python language. + +Based on JSON.minify.js: https://github.com/getify/JSON.minify + +Contributers: + - Gerald Storer + - Contributed original version + - Felipe Machado + - Performance optimization + - Pradyun S. Gedam + - Conditions and variable names changed + - Reformatted tests and moved to separate file + - Made into a PyPI Package +""" + +import re + + +# Vimspector modification: strip_space defaults to False; we don't actually want +# to minify - we just want to strip comments. +def minify(string, strip_space=False): + tokenizer = re.compile('"|(/\*)|(\*/)|(//)|\n|\r') + end_slashes_re = re.compile(r'(\\)*$') + + in_string = False + in_multi = False + in_single = False + + new_str = [] + index = 0 + + for match in re.finditer(tokenizer, string): + + if not (in_multi or in_single): + tmp = string[index:match.start()] + if not in_string and strip_space: + # replace white space as defined in standard + tmp = re.sub('[ \t\n\r]+', '', tmp) + new_str.append(tmp) + elif not strip_space: + # Replace comments with white space so that the JSON parser reports + # the correct column numbers on parsing errors. + new_str.append(' ' * (match.start() - index)) + + index = match.end() + val = match.group() + + if val == '"' and not (in_multi or in_single): + escaped = end_slashes_re.search(string, 0, match.start()) + + # start of string or unescaped quote character to end string + if not in_string or (escaped is None or len(escaped.group()) % 2 == 0): # noqa + in_string = not in_string + index -= 1 # include " character in next catch + elif not (in_string or in_multi or in_single): + if val == '/*': + in_multi = True + elif val == '//': + in_single = True + elif val == '*/' and in_multi and not (in_string or in_single): + in_multi = False + if not strip_space: + new_str.append(' ' * len(val)) + elif val in '\r\n' and not (in_multi or in_string) and in_single: + in_single = False + elif not ((in_multi or in_single) or (val in ' \r\n\t' and strip_space)): # noqa + new_str.append(val) + + if not strip_space: + if val in '\r\n': + new_str.append(val) + elif in_multi or in_single: + new_str.append(' ' * len(val)) + + new_str.append(string[index:]) + return ''.join(new_str) diff --git a/run_tests b/run_tests index f2f8022..201ec1b 100755 --- a/run_tests +++ b/run_tests @@ -1,12 +1,130 @@ #!/usr/bin/env bash -RUN_VIM="vim --clean --not-a-term" +SetBaseDir() { + BASEDIR=$1 + if [[ ! $BASEDIR = /* ]]; then + # Relative + BASEDIR=$(pwd)/${BASEDIR} + fi +} + +SetBaseDir $(dirname $0) +INSTALL=0 +UPDATE=0 +INSTALLER_ARGS='' +RUN_VIM="vim -N --clean --not-a-term" RUN_TEST="${RUN_VIM} -S lib/run_test.vim" +BASEDIR_CMD='py3 pass' + +# 1 is stdout +out_fd=1 + +while [ -n "$1" ]; do + case "$1" in + "--basedir"|"--base-dir"|"--test-base") + shift + SetBaseDir $1 + shift + BASEDIR_CMD="let g:vimspector_base_dir='${BASEDIR}'" + ;; + "--install") + INSTALL=1 + shift + ;; + "--install-method") + shift + INSTALL=$1 + shift + ;; + "--update"|"--upgrade") + UPDATE=1 + shift + ;; + "--report") + shift + VIMSPECTOR_TEST_STDOUT=$1 + shift + ;; + "--quiet") + shift + # send the output to /dev/null + # https://stackoverflow.com/a/47553900 + # Note we can't use {out_fd} here because the bash version in CI is too + # old on macOS + out_fd=3 + exec 3>/dev/null + INSTALLER_ARGS="${INSTALLER_ARGS} --quiet" + ;; + "--") + shift + break + ;; + "--help") + shift + echo "$(basename $0) [--basedir ] [--report output] [--quiet] [--install] " + echo "" + echo " --basedir path to runtime directory like the optino to install_gadget.py" + echo " --install run install_gadget.py, useful with --basedir" + echo " --report which logs to dump to stdout after a test" + echo " --quiet suppress vim's stdout" + echo "e.g.: " + echo " - run all tests: $0" + echo " - run specific tests script: $0 signature_help.test.vim" + echo " - run specific tests fun: $0 signature_help.test.vim:Test_signatures_TopLine\(\)" + echo " - run all tests in a clean env: $0 --basedir \$(mktemp -d) --install" + exit 0 + ;; + *) + break + ;; + esac +done + +# We use fd 3 for vim's output. Send it to stdout if not already redirected +# Note: can't use ${out_fd} in a redirect because redirects happen before +# variable substitution +if [ "${out_fd}" = "1" ]; then + exec 3>&1 +fi + +if [ "$INSTALL" = "1" ] || [ "$INSTALL" = "script" ]; then + if ! python3 $(dirname $0)/install_gadget.py \ + --basedir ${BASEDIR} \ + ${INSTALLER_ARGS} \ + --all \ + --force-enable-csharp; then + echo "Script installation reported errors" >&2 + exit 1 + fi +fi + +if [ "$INSTALL" = "1" ] || [ "$INSTALL" = "vim" ]; then + if ! $RUN_VIM -u $(dirname $0)/tests/vimrc \ + --cmd "${BASEDIR_CMD}" \ + -c 'autocmd User VimspectorInstallSuccess qa!' \ + -c 'autocmd User VimspectorInstallFailed cquit!' \ + -c "VimspectorInstall --all netcoredbg"; then + echo "Vim installation reported errors" >&2 + exit 1 + fi +fi + +if [ "$UPDATE" = "1" ]; then + if ! $RUN_VIM -u $(dirname $0)/tests/vimrc \ + --cmd "${BASEDIR_CMD}" \ + -c 'autocmd User VimspectorInstallSuccess qa!' \ + -c 'autocmd User VimspectorInstallFailed cquit!' \ + -c "VimspectorUpdate"; then + echo "Vim update reported errors" >&2 + exit 1 + fi +fi + if [ -z "$VIMSPECTOR_MIMODE" ]; then - if which -s lldb; then + if which lldb >/dev/null 2>&1; then export VIMSPECTOR_MIMODE=lldb - elif which -s gdb; then + elif which gdb >/dev/null 2>&1; then export VIMSPECTOR_MIMODE=gdb else echo "Couldn't guess VIMSPECTOR_MIMODE. Need lldb or gdb in path" @@ -14,18 +132,27 @@ if [ -z "$VIMSPECTOR_MIMODE" ]; then fi fi -echo "Testing with VIMSPECTOR_MIMODE=$VIMSPECTOR_MIMODE" +echo "Testing with:" +echo " * VIMSPECTOR_MIMODE=$VIMSPECTOR_MIMODE" +echo " * RUN_VIM=$RUN_VIM" +echo " * RUN_TEST=$RUN_TEST" +echo " * BASEDIR=$BASEDIR" +echo " * BASEDIR_CMD=$BASEDIR_CMD" + echo "%SETUP - Building test programs..." set -e pushd tests/testdata/cpp/simple - make clean simple + make clean all + popd + pushd support/test/csharp + dotnet build popd set +e echo "%DONE - built test programs" -pushd tests > /dev/null - +# Start +pushd $(dirname $0)/tests > /dev/null echo "Running Vimspector Vim tests" RESULT=0 @@ -39,23 +166,62 @@ fi for t in ${TESTS}; do echo "" echo "%RUN: $t" - rm -f messages debuglog # split on : into fileName and testName IFS=: read -s t T <<< "$t" - if ${RUN_TEST} --cmd 'au SwapExists * let v:swapchoice = "e"' $t $T; then + TESTLOGDIR=${BASEDIR}/tests/logs/$t + + if ${RUN_TEST} --cmd "${BASEDIR_CMD}" \ + --cmd 'au SwapExists * let v:swapchoice = "e"' $t $T \ + >&3\ + && [ -f $t.res ]; then echo "%PASS: $t PASSED" else - cat messages - echo "%FAIL: $t FAILED" + echo "%FAIL: $t FAILED - see $TESTLOGDIR" RESULT=1 fi + + rm -rf $TESTLOGDIR + mkdir -p $TESTLOGDIR + ${RUN_VIM} --version > ${TESTLOGDIR}/vimversion + if [ "$VIMSPECTOR_TEST_STDOUT" = "messages" ]; then + if [ -f messages ]; then + echo "%MESSAGES" + cat messages + echo "%END" + else + echo "%MESSAGES" + echo "No messages found" + echo "%END" + fi + fi + + for l in messages debuglog test.log *.testlog; do + # In CI we can't view the output files, so we just have to cat them + if [ -f $l ]; then + if [ "$VIMSPECTOR_TEST_STDOUT" = "all" ]; then + echo "" + echo "" + echo "*** START: $l ***" + cat $l + echo "*** END: $l ***" + fi + mv $l $TESTLOGDIR + fi + done + + rm -f $t.res done +# close out_fd if it's not stdout/stderr/ +(( out_fd > 2 )) && exec 3>&- + + +echo "Done running tests" popd > /dev/null echo "" -echo "All done." +echo "All done. Exit with ${RESULT}" exit $RESULT diff --git a/support/custom_ui_vimrc b/support/custom_ui_vimrc new file mode 100644 index 0000000..e76c6ee --- /dev/null +++ b/support/custom_ui_vimrc @@ -0,0 +1,161 @@ +" setup boilerplate to make this file usable with vim -Nu {{{ +scriptencoding utf-8 +execute 'source' expand( ':p:h' ) . '/minimal_vimrc' +set noequalalways +let mapleader = ',' +let maplocalleader = "\" +" }}} + +" Custom Layout {{{ + +function! s:CustomiseUI() + let wins = g:vimspector_session_windows + + " Close the Variables window + if has( 'nvim' ) + " No win_execute in neovim + call win_gotoid( wins.variables ) + quit + else + call win_execute( wins.variables, 'q' ) + endif + + " Put the stack trace at the top of the "left bar" (rotate) + call win_gotoid( wins.stack_trace ) + wincmd r + + " Make the left column at least 70 chars + 70wincmd | + + " Make the code window at least 80 chars + call win_gotoid( wins.code ) + 80wincmd | + + " Make the output window 10 lines high and right at the top of the screen + call win_gotoid( wins.output ) + 10wincmd _ + wincmd K +endfunction + +function s:SetUpTerminal() + if !has_key( g:vimspector_session_windows, 'terminal' ) + " There's a neovim bug which means that this doesn't work in neovim + return + endif + let terminal_win = g:vimspector_session_windows.terminal + + " Make the terminal window at most 80 columns wide, ensuring there is enough + " sapce for our code window (80 columns) and the left bar (70 columns) + + " Padding is 2 for the 2 vertical split markers and 2 for the sign column in + " the code window. + let left_bar = 70 + let code = 80 + let padding = 4 + let cols = max( [ min( [ &columns - left_bar - code - padding, 80 ] ), 10 ] ) + call win_gotoid( terminal_win ) + execute string(cols) . 'wincmd |' +endfunction + +function! s:CustomiseWinBar() + call win_gotoid( g:vimspector_session_windows.code) + aunmenu WinBar + nnoremenu WinBar.▷\ ᶠ⁵ :call vimspector#Continue() + nnoremenu WinBar.↷\ ᶠ¹⁰ :call vimspector#StepOver() + nnoremenu WinBar.↓\ ᶠ¹¹ :call vimspector#StepInto() + nnoremenu WinBar.↑\ ˢᶠ¹¹ :call vimspector#StepOut() + nnoremenu WinBar.❘❘\ ᶠ⁶ :call vimspector#Pause() + nnoremenu WinBar.□\ ˢᶠ⁵ :call vimspector#Stop() + nnoremenu WinBar.⟲\ ᶜˢᶠ⁵ :call vimspector#Restart() + nnoremenu WinBar.✕\ ᶠ⁸ :call vimspector#Reset() +endfunction + +augroup TestUICustomistaion + autocmd! + autocmd User VimspectorUICreated call s:CustomiseUI() + autocmd User VimspectorTerminalOpened call s:SetUpTerminal() + autocmd User VimspectorUICreated call s:CustomiseWinBar() +augroup END + +" }}} + +" Custom sign priority {{{ + +let g:vimspector_sign_priority = { + \ 'vimspectorBP': 3, + \ 'vimspectorBPCond': 2, + \ 'vimspectorBPDisabled': 1, + \ 'vimspectorPC': 999, + \ } + +" }}} + +" Custom mappings while debuggins {{{ +let s:mapped = {} + +function! s:OnJumpToFrame() abort + if has_key( s:mapped, string( bufnr() ) ) + return + endif + + nmap dn VimspectorStepOver + nmap ds VimspectorStepInto + nmap df VimspectorStepOut + nmap dc VimspectorContinue + nmap di VimspectorBalloonEval + xmap di VimspectorBalloonEval + + let s:mapped[ string( bufnr() ) ] = { 'modifiable': &modifiable } + + setlocal nomodifiable + +endfunction + +function! s:OnDebugEnd() abort + + let original_buf = bufnr() + let hidden = &hidden + + try + set hidden + for bufnr in keys( s:mapped ) + try + execute 'noautocmd buffer' bufnr + silent! nunmap dn + silent! nunmap ds + silent! nunmap df + silent! nunmap dc + silent! nunmap di + silent! xunmap di + + let &l:modifiable = s:mapped[ bufnr ][ 'modifiable' ] + endtry + endfor + finally + execute 'noautocmd buffer' original_buf + let &hidden = hidden + endtry + + let s:mapped = {} +endfunction + +augroup TestCustomMappings + au! + autocmd User VimspectorJumpedToFrame call s:OnJumpToFrame() + autocmd User VimspectorDebugEnded call s:OnDebugEnd() +augroup END + +" }}} + +" Custom mappings for special buffers {{{ + +let g:vimspector_mappings = { + \ 'stack_trace': {}, + \ 'variables': { + \ 'set_value': [ '', '', 'C' ], + \ } + \ } + +" }}} + +" vim: foldmethod=marker diff --git a/support/gadget_upgrade/README.md b/support/gadget_upgrade/README.md new file mode 100644 index 0000000..9ae3d7f --- /dev/null +++ b/support/gadget_upgrade/README.md @@ -0,0 +1,8 @@ +# Manually updating shipped gadgets + +Download the gadget files manuall from their official source into this dir. +Run `./checksum.py ` to get the checksums. + +Update ../../python3/vimspector/gadgets.py with the new version and the +checksums. + diff --git a/support/gadget_upgrade/checksum.py b/support/gadget_upgrade/checksum.py new file mode 100755 index 0000000..d0c1404 --- /dev/null +++ b/support/gadget_upgrade/checksum.py @@ -0,0 +1,13 @@ +#!/usr/bin/env python3 + +import hashlib +import sys + + +def GetChecksumSHA254( file_path ): + with open( file_path, 'rb' ) as existing_file: + return hashlib.sha256( existing_file.read() ).hexdigest() + + +for arg in sys.argv[ 1: ]: + print( f"{ arg } = { GetChecksumSHA254( arg ) }" ) diff --git a/support/minimal_vimrc b/support/minimal_vimrc new file mode 100644 index 0000000..3626d39 --- /dev/null +++ b/support/minimal_vimrc @@ -0,0 +1,4 @@ +let s:vimspector_path = expand( ':p:h:h' ) +let g:vimspector_enable_mappings = 'HUMAN' +exe 'source ' . s:vimspector_path . '/tests/vimrc' + diff --git a/support/test/bash/.vimspector.json b/support/test/bash/.vimspector.json new file mode 100644 index 0000000..a1be1b9 --- /dev/null +++ b/support/test/bash/.vimspector.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://puremourning.github.io/vimspector/schema/vimspector.schema.json", + "configurations": { + "Run Current Script": { + "adapter": "vscode-bash", + "autoselect": false, + "configuration": { + "request": "launch", + "program": "${file}", + "cwd": "${fileDirname}", + "args": [ "*${args}" ] + } + } + } +} diff --git a/support/test/bash/test_script b/support/test/bash/test_script new file mode 100644 index 0000000..4e27bd9 --- /dev/null +++ b/support/test/bash/test_script @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +function Test() { + echo $1 +} + +for i in "$@"; do + Test $i +done diff --git a/support/test/chrome/run_server b/support/test/chrome/run_server new file mode 100755 index 0000000..9e5b569 --- /dev/null +++ b/support/test/chrome/run_server @@ -0,0 +1,3 @@ +#!/usr/bin/env bash + +php -S localhost:1234 -t www diff --git a/support/test/chrome/www/js/test.js b/support/test/chrome/www/js/test.js index 273804e..9c1c4a1 100644 --- a/support/test/chrome/www/js/test.js +++ b/support/test/chrome/www/js/test.js @@ -6,5 +6,12 @@ $( document ).ready( function() { return msg; }; - alert( 'test: ' + getMessage() ); + var obj = { + test: getMessage(), + toast: function() { return 'egg'; }, + spam: 'ham' + }; + + alert( 'test: ' + obj.test ); + alert( 'toast: ' + obj.toast() ); } ); diff --git a/support/test/cpp/simple_c_program/.vimspector.json b/support/test/cpp/simple_c_program/.vimspector.json index 5f1ce6c..fb4c958 100644 --- a/support/test/cpp/simple_c_program/.vimspector.json +++ b/support/test/cpp/simple_c_program/.vimspector.json @@ -1,36 +1,27 @@ { - "adapters": { - "cppdbg": { - "name": "cppdbg", - "command": [ "$HOME/.vscode/extensions/ms-vscode.cpptools-0.20.1/debugAdapters/OpenDebugAD7" ], - "attach": { - "pidProperty": "processId", - "pidSelect": "ask" - } - } - }, "configurations": { - "simple_c_program - Launch": { - "adapter": "cppdbg", + "CodeLLDB": { + "adapter": "CodeLLDB", "configuration": { - "name": "ms Launch", - "type": "cppdbg", "request": "launch", "program": "${workspaceRoot}/test", - "args": [], - "cwd": "${workspaceRoot}", - "environment": [], - "MIMode": "lldb", "stopAtEntry": true } }, - "simple_c_program - Attach": { - "adapter": "cppdbg", + "lldb-vscode": { + "adapter": "lldb-vscode", "configuration": { - "name": "(lldb) Attach", - "type": "cppdbg", - "request": "attach", + "request": "launch", "program": "${workspaceRoot}/test", + "stopAtEntry": true + } + }, + "cpptools": { + "adapter": "vscode-cpptools", + "configuration": { + "request": "launch", + "program": "${workspaceRoot}/test", + "stopOnEntry": true, "MIMode": "lldb" } } diff --git a/support/test/cpp/simple_c_program/.ycm_extra_conf.py b/support/test/cpp/simple_c_program/.ycm_extra_conf.py index 0d17586..4203b36 100644 --- a/support/test/cpp/simple_c_program/.ycm_extra_conf.py +++ b/support/test/cpp/simple_c_program/.ycm_extra_conf.py @@ -1,4 +1,4 @@ def Settings( **kwargs ): return { - 'flags': [ '-x', 'c++', '-Wall', '-Wextra' ] + 'flags': [ '-x', 'c++', '-Wall', '-Wextra', '-std=c++17' ] } diff --git a/support/test/cpp/simple_c_program/test_c.cpp b/support/test/cpp/simple_c_program/test_c.cpp index 4a95936..8dcc2dc 100644 --- a/support/test/cpp/simple_c_program/test_c.cpp +++ b/support/test/cpp/simple_c_program/test_c.cpp @@ -1,3 +1,5 @@ +#include +#include #include namespace Test @@ -33,6 +35,8 @@ int main ( int argc, char ** argv ) { int x{ 10 }; + printf( "HOME: %s\n", getenv( "HOME" ) ); + Test::TestStruct t{ true, {99} }; foo( t ); } diff --git a/support/test/csharp/.gitignore b/support/test/csharp/.gitignore index b7d74e4..03cd7d8 100644 --- a/support/test/csharp/.gitignore +++ b/support/test/csharp/.gitignore @@ -1,2 +1,3 @@ bin/ obj/Debug +obj/ diff --git a/support/test/csharp/.vimspector.json b/support/test/csharp/.vimspector.json index d5a68b6..326739b 100644 --- a/support/test/csharp/.vimspector.json +++ b/support/test/csharp/.vimspector.json @@ -1,21 +1,57 @@ { - "configurations": { - "launch - netcoredbg": { - "adapter": "netcoredbg", - "configuration": { - "request": "launch", - "program": "${workspaceRoot}/bin/Debug/netcoreapp2.2/csharp.dll", - "args": [], - "stopAtEntry": true - } + "adapters": { + "netcoredbg-debuglog": { + "attach": { + "pidProperty": "processId", + "pidSelect": "ask" }, - "launch - mono": { - "adapter": "vscode-mono-debug", - "configuration": { - "request": "launch", - "program": "${workspaceRoot}/Program.exe" - } + "command": [ + "${gadgetDir}/netcoredbg/netcoredbg", + "--interpreter=vscode", + "--engineLogging=${workspaceRoot}/netcoredbg.engine.log", + "--log=${workspaceRoot}/netcoredbg.log" + ], + "configuration": { + "cwd": "${workspaceRoot}" + }, + "name": "netcoredbg" + } + }, + "configurations": { + // + // NOTE: + // If you add to this, you must update tests/get_configurations.test.vim + // + + "launch - netcoredbg": { + "adapter": "netcoredbg", + "configuration": { + "request": "launch", + "program": "${workspaceRoot}/bin/Debug/netcoreapp3.1/csharp.dll", + "args": [], + "stopAtEntry": false + } + }, + "launch - netcoredbg - with debug log": { + "adapter": "netcoredbg-debuglog", + "configuration": { + "request": "launch", + "program": "${workspaceRoot}/bin/Debug/netcoreapp3.1/csharp.dll", + "args": [], + "stopAtEntry": false + } + }, + "launch - mono": { + "adapter": "vscode-mono-debug", + "configuration": { + "request": "launch", + "program": "${workspaceRoot}/Program.exe", + "console": "integratedTerminal", + "cwd": "${workspaceRoot}", + "args": [], + "env": {} } } + } } diff --git a/support/test/csharp/Program.cs b/support/test/csharp/Program.cs index 260d2f4..ff90ed6 100644 --- a/support/test/csharp/Program.cs +++ b/support/test/csharp/Program.cs @@ -2,11 +2,36 @@ namespace csharp { - class Program - { - static void Main(string[] args) - { - Console.WriteLine("Hello World!"); - } + class Program + { + string toaster = "Making round of toast"; + static int max_bread = 100; + int bread = max_bread; + + void PrintToast( int r ) { + int this_round = ( max_bread - bread - r); + Console.WriteLine( this.toaster + ": " + this_round ); } + + void MakeToast( int rounds ) { + if (this.bread - rounds < 0) { + throw new Exception( "No moar bread!" ); + } + + this.bread -= rounds; + for (int r = 0; r < rounds; ++r) { + this.PrintToast( r ); + } + + Console.WriteLine( "Got only " + this.bread + " left" ); + } + + static void Main(string[] args) + { + Program p = new Program(); + for (int x = 1; x < 10; ++ x) { + p.MakeToast( x ); + } + } + } } diff --git a/support/test/csharp/csharp.csproj b/support/test/csharp/csharp.csproj index 01d5113..d453e9a 100644 --- a/support/test/csharp/csharp.csproj +++ b/support/test/csharp/csharp.csproj @@ -2,7 +2,7 @@ Exe - netcoreapp2.2 + netcoreapp3.1 diff --git a/support/test/csharp/csharp.sln b/support/test/csharp/csharp.sln index bba50e0..91f59bf 100644 --- a/support/test/csharp/csharp.sln +++ b/support/test/csharp/csharp.sln @@ -3,6 +3,8 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 VisualStudioVersion = 15.0.26124.0 MinimumVisualStudioVersion = 15.0.26124.0 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "csharp", "csharp.csproj", "{91DB205F-E422-430B-BBB8-955110C7B3B6}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -15,4 +17,18 @@ Global GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {91DB205F-E422-430B-BBB8-955110C7B3B6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {91DB205F-E422-430B-BBB8-955110C7B3B6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {91DB205F-E422-430B-BBB8-955110C7B3B6}.Debug|x64.ActiveCfg = Debug|Any CPU + {91DB205F-E422-430B-BBB8-955110C7B3B6}.Debug|x64.Build.0 = Debug|Any CPU + {91DB205F-E422-430B-BBB8-955110C7B3B6}.Debug|x86.ActiveCfg = Debug|Any CPU + {91DB205F-E422-430B-BBB8-955110C7B3B6}.Debug|x86.Build.0 = Debug|Any CPU + {91DB205F-E422-430B-BBB8-955110C7B3B6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {91DB205F-E422-430B-BBB8-955110C7B3B6}.Release|Any CPU.Build.0 = Release|Any CPU + {91DB205F-E422-430B-BBB8-955110C7B3B6}.Release|x64.ActiveCfg = Release|Any CPU + {91DB205F-E422-430B-BBB8-955110C7B3B6}.Release|x64.Build.0 = Release|Any CPU + {91DB205F-E422-430B-BBB8-955110C7B3B6}.Release|x86.ActiveCfg = Release|Any CPU + {91DB205F-E422-430B-BBB8-955110C7B3B6}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection EndGlobal diff --git a/support/test/csharp/obj/csharp.csproj.nuget.cache b/support/test/csharp/obj/csharp.csproj.nuget.cache deleted file mode 100644 index 3ac8d84..0000000 --- a/support/test/csharp/obj/csharp.csproj.nuget.cache +++ /dev/null @@ -1,5 +0,0 @@ -{ - "version": 1, - "dgSpecHash": "6/vdr7YprlSIoQecv/nNuLNflFpO0X7eN7jHUinZTsgian9nYpmHMWirsDWMi5l+29TH+Qy8O/QfaB/48QtjRQ==", - "success": true -} \ No newline at end of file diff --git a/support/test/csharp/obj/csharp.csproj.nuget.g.props b/support/test/csharp/obj/csharp.csproj.nuget.g.props deleted file mode 100644 index 4751d88..0000000 --- a/support/test/csharp/obj/csharp.csproj.nuget.g.props +++ /dev/null @@ -1,18 +0,0 @@ - - - - True - NuGet - /Users/ben/.vim/bundle/vimspector/support/test/csharp/obj/project.assets.json - /Users/ben/.nuget/packages/ - /Users/ben/.nuget/packages/;/usr/local/share/dotnet/sdk/NuGetFallbackFolder - PackageReference - 4.9.4 - - - $(MSBuildAllProjects);$(MSBuildThisFileFullPath) - - - - - \ No newline at end of file diff --git a/support/test/csharp/obj/csharp.csproj.nuget.g.targets b/support/test/csharp/obj/csharp.csproj.nuget.g.targets deleted file mode 100644 index 099158b..0000000 --- a/support/test/csharp/obj/csharp.csproj.nuget.g.targets +++ /dev/null @@ -1,10 +0,0 @@ - - - - $(MSBuildAllProjects);$(MSBuildThisFileFullPath) - - - - - - \ No newline at end of file diff --git a/support/test/csharp/obj/project.assets.json b/support/test/csharp/obj/project.assets.json deleted file mode 100644 index e6c9b94..0000000 --- a/support/test/csharp/obj/project.assets.json +++ /dev/null @@ -1,742 +0,0 @@ -{ - "version": 3, - "targets": { - ".NETCoreApp,Version=v2.2": { - "Microsoft.NETCore.App/2.2.0": { - "type": "package", - "dependencies": { - "Microsoft.NETCore.DotNetHostPolicy": "2.2.0", - "Microsoft.NETCore.Platforms": "2.2.0", - "Microsoft.NETCore.Targets": "2.0.0", - "NETStandard.Library": "2.0.3" - }, - "compile": { - "ref/netcoreapp2.2/Microsoft.CSharp.dll": {}, - "ref/netcoreapp2.2/Microsoft.VisualBasic.dll": {}, - "ref/netcoreapp2.2/Microsoft.Win32.Primitives.dll": {}, - "ref/netcoreapp2.2/System.AppContext.dll": {}, - "ref/netcoreapp2.2/System.Buffers.dll": {}, - "ref/netcoreapp2.2/System.Collections.Concurrent.dll": {}, - "ref/netcoreapp2.2/System.Collections.Immutable.dll": {}, - "ref/netcoreapp2.2/System.Collections.NonGeneric.dll": {}, - "ref/netcoreapp2.2/System.Collections.Specialized.dll": {}, - "ref/netcoreapp2.2/System.Collections.dll": {}, - "ref/netcoreapp2.2/System.ComponentModel.Annotations.dll": {}, - "ref/netcoreapp2.2/System.ComponentModel.DataAnnotations.dll": {}, - "ref/netcoreapp2.2/System.ComponentModel.EventBasedAsync.dll": {}, - "ref/netcoreapp2.2/System.ComponentModel.Primitives.dll": {}, - "ref/netcoreapp2.2/System.ComponentModel.TypeConverter.dll": {}, - "ref/netcoreapp2.2/System.ComponentModel.dll": {}, - "ref/netcoreapp2.2/System.Configuration.dll": {}, - "ref/netcoreapp2.2/System.Console.dll": {}, - "ref/netcoreapp2.2/System.Core.dll": {}, - "ref/netcoreapp2.2/System.Data.Common.dll": {}, - "ref/netcoreapp2.2/System.Data.dll": {}, - "ref/netcoreapp2.2/System.Diagnostics.Contracts.dll": {}, - "ref/netcoreapp2.2/System.Diagnostics.Debug.dll": {}, - "ref/netcoreapp2.2/System.Diagnostics.DiagnosticSource.dll": {}, - "ref/netcoreapp2.2/System.Diagnostics.FileVersionInfo.dll": {}, - "ref/netcoreapp2.2/System.Diagnostics.Process.dll": {}, - "ref/netcoreapp2.2/System.Diagnostics.StackTrace.dll": {}, - "ref/netcoreapp2.2/System.Diagnostics.TextWriterTraceListener.dll": {}, - "ref/netcoreapp2.2/System.Diagnostics.Tools.dll": {}, - "ref/netcoreapp2.2/System.Diagnostics.TraceSource.dll": {}, - "ref/netcoreapp2.2/System.Diagnostics.Tracing.dll": {}, - "ref/netcoreapp2.2/System.Drawing.Primitives.dll": {}, - "ref/netcoreapp2.2/System.Drawing.dll": {}, - "ref/netcoreapp2.2/System.Dynamic.Runtime.dll": {}, - "ref/netcoreapp2.2/System.Globalization.Calendars.dll": {}, - "ref/netcoreapp2.2/System.Globalization.Extensions.dll": {}, - "ref/netcoreapp2.2/System.Globalization.dll": {}, - "ref/netcoreapp2.2/System.IO.Compression.Brotli.dll": {}, - "ref/netcoreapp2.2/System.IO.Compression.FileSystem.dll": {}, - "ref/netcoreapp2.2/System.IO.Compression.ZipFile.dll": {}, - "ref/netcoreapp2.2/System.IO.Compression.dll": {}, - "ref/netcoreapp2.2/System.IO.FileSystem.DriveInfo.dll": {}, - "ref/netcoreapp2.2/System.IO.FileSystem.Primitives.dll": {}, - "ref/netcoreapp2.2/System.IO.FileSystem.Watcher.dll": {}, - "ref/netcoreapp2.2/System.IO.FileSystem.dll": {}, - "ref/netcoreapp2.2/System.IO.IsolatedStorage.dll": {}, - "ref/netcoreapp2.2/System.IO.MemoryMappedFiles.dll": {}, - "ref/netcoreapp2.2/System.IO.Pipes.dll": {}, - "ref/netcoreapp2.2/System.IO.UnmanagedMemoryStream.dll": {}, - "ref/netcoreapp2.2/System.IO.dll": {}, - "ref/netcoreapp2.2/System.Linq.Expressions.dll": {}, - "ref/netcoreapp2.2/System.Linq.Parallel.dll": {}, - "ref/netcoreapp2.2/System.Linq.Queryable.dll": {}, - "ref/netcoreapp2.2/System.Linq.dll": {}, - "ref/netcoreapp2.2/System.Memory.dll": {}, - "ref/netcoreapp2.2/System.Net.Http.dll": {}, - "ref/netcoreapp2.2/System.Net.HttpListener.dll": {}, - "ref/netcoreapp2.2/System.Net.Mail.dll": {}, - "ref/netcoreapp2.2/System.Net.NameResolution.dll": {}, - "ref/netcoreapp2.2/System.Net.NetworkInformation.dll": {}, - "ref/netcoreapp2.2/System.Net.Ping.dll": {}, - "ref/netcoreapp2.2/System.Net.Primitives.dll": {}, - "ref/netcoreapp2.2/System.Net.Requests.dll": {}, - "ref/netcoreapp2.2/System.Net.Security.dll": {}, - "ref/netcoreapp2.2/System.Net.ServicePoint.dll": {}, - "ref/netcoreapp2.2/System.Net.Sockets.dll": {}, - "ref/netcoreapp2.2/System.Net.WebClient.dll": {}, - "ref/netcoreapp2.2/System.Net.WebHeaderCollection.dll": {}, - "ref/netcoreapp2.2/System.Net.WebProxy.dll": {}, - "ref/netcoreapp2.2/System.Net.WebSockets.Client.dll": {}, - "ref/netcoreapp2.2/System.Net.WebSockets.dll": {}, - "ref/netcoreapp2.2/System.Net.dll": {}, - "ref/netcoreapp2.2/System.Numerics.Vectors.dll": {}, - "ref/netcoreapp2.2/System.Numerics.dll": {}, - "ref/netcoreapp2.2/System.ObjectModel.dll": {}, - "ref/netcoreapp2.2/System.Reflection.DispatchProxy.dll": {}, - "ref/netcoreapp2.2/System.Reflection.Emit.ILGeneration.dll": {}, - "ref/netcoreapp2.2/System.Reflection.Emit.Lightweight.dll": {}, - "ref/netcoreapp2.2/System.Reflection.Emit.dll": {}, - "ref/netcoreapp2.2/System.Reflection.Extensions.dll": {}, - "ref/netcoreapp2.2/System.Reflection.Metadata.dll": {}, - "ref/netcoreapp2.2/System.Reflection.Primitives.dll": {}, - "ref/netcoreapp2.2/System.Reflection.TypeExtensions.dll": {}, - "ref/netcoreapp2.2/System.Reflection.dll": {}, - "ref/netcoreapp2.2/System.Resources.Reader.dll": {}, - "ref/netcoreapp2.2/System.Resources.ResourceManager.dll": {}, - "ref/netcoreapp2.2/System.Resources.Writer.dll": {}, - "ref/netcoreapp2.2/System.Runtime.CompilerServices.VisualC.dll": {}, - "ref/netcoreapp2.2/System.Runtime.Extensions.dll": {}, - "ref/netcoreapp2.2/System.Runtime.Handles.dll": {}, - "ref/netcoreapp2.2/System.Runtime.InteropServices.RuntimeInformation.dll": {}, - "ref/netcoreapp2.2/System.Runtime.InteropServices.WindowsRuntime.dll": {}, - "ref/netcoreapp2.2/System.Runtime.InteropServices.dll": {}, - "ref/netcoreapp2.2/System.Runtime.Loader.dll": {}, - "ref/netcoreapp2.2/System.Runtime.Numerics.dll": {}, - "ref/netcoreapp2.2/System.Runtime.Serialization.Formatters.dll": {}, - "ref/netcoreapp2.2/System.Runtime.Serialization.Json.dll": {}, - "ref/netcoreapp2.2/System.Runtime.Serialization.Primitives.dll": {}, - "ref/netcoreapp2.2/System.Runtime.Serialization.Xml.dll": {}, - "ref/netcoreapp2.2/System.Runtime.Serialization.dll": {}, - "ref/netcoreapp2.2/System.Runtime.dll": {}, - "ref/netcoreapp2.2/System.Security.Claims.dll": {}, - "ref/netcoreapp2.2/System.Security.Cryptography.Algorithms.dll": {}, - "ref/netcoreapp2.2/System.Security.Cryptography.Csp.dll": {}, - "ref/netcoreapp2.2/System.Security.Cryptography.Encoding.dll": {}, - "ref/netcoreapp2.2/System.Security.Cryptography.Primitives.dll": {}, - "ref/netcoreapp2.2/System.Security.Cryptography.X509Certificates.dll": {}, - "ref/netcoreapp2.2/System.Security.Principal.dll": {}, - "ref/netcoreapp2.2/System.Security.SecureString.dll": {}, - "ref/netcoreapp2.2/System.Security.dll": {}, - "ref/netcoreapp2.2/System.ServiceModel.Web.dll": {}, - "ref/netcoreapp2.2/System.ServiceProcess.dll": {}, - "ref/netcoreapp2.2/System.Text.Encoding.Extensions.dll": {}, - "ref/netcoreapp2.2/System.Text.Encoding.dll": {}, - "ref/netcoreapp2.2/System.Text.RegularExpressions.dll": {}, - "ref/netcoreapp2.2/System.Threading.Overlapped.dll": {}, - "ref/netcoreapp2.2/System.Threading.Tasks.Dataflow.dll": {}, - "ref/netcoreapp2.2/System.Threading.Tasks.Extensions.dll": {}, - "ref/netcoreapp2.2/System.Threading.Tasks.Parallel.dll": {}, - "ref/netcoreapp2.2/System.Threading.Tasks.dll": {}, - "ref/netcoreapp2.2/System.Threading.Thread.dll": {}, - "ref/netcoreapp2.2/System.Threading.ThreadPool.dll": {}, - "ref/netcoreapp2.2/System.Threading.Timer.dll": {}, - "ref/netcoreapp2.2/System.Threading.dll": {}, - "ref/netcoreapp2.2/System.Transactions.Local.dll": {}, - "ref/netcoreapp2.2/System.Transactions.dll": {}, - "ref/netcoreapp2.2/System.ValueTuple.dll": {}, - "ref/netcoreapp2.2/System.Web.HttpUtility.dll": {}, - "ref/netcoreapp2.2/System.Web.dll": {}, - "ref/netcoreapp2.2/System.Windows.dll": {}, - "ref/netcoreapp2.2/System.Xml.Linq.dll": {}, - "ref/netcoreapp2.2/System.Xml.ReaderWriter.dll": {}, - "ref/netcoreapp2.2/System.Xml.Serialization.dll": {}, - "ref/netcoreapp2.2/System.Xml.XDocument.dll": {}, - "ref/netcoreapp2.2/System.Xml.XPath.XDocument.dll": {}, - "ref/netcoreapp2.2/System.Xml.XPath.dll": {}, - "ref/netcoreapp2.2/System.Xml.XmlDocument.dll": {}, - "ref/netcoreapp2.2/System.Xml.XmlSerializer.dll": {}, - "ref/netcoreapp2.2/System.Xml.dll": {}, - "ref/netcoreapp2.2/System.dll": {}, - "ref/netcoreapp2.2/WindowsBase.dll": {}, - "ref/netcoreapp2.2/mscorlib.dll": {}, - "ref/netcoreapp2.2/netstandard.dll": {} - }, - "build": { - "build/netcoreapp2.2/Microsoft.NETCore.App.props": {}, - "build/netcoreapp2.2/Microsoft.NETCore.App.targets": {} - } - }, - "Microsoft.NETCore.DotNetAppHost/2.2.0": { - "type": "package" - }, - "Microsoft.NETCore.DotNetHostPolicy/2.2.0": { - "type": "package", - "dependencies": { - "Microsoft.NETCore.DotNetHostResolver": "2.2.0" - } - }, - "Microsoft.NETCore.DotNetHostResolver/2.2.0": { - "type": "package", - "dependencies": { - "Microsoft.NETCore.DotNetAppHost": "2.2.0" - } - }, - "Microsoft.NETCore.Platforms/2.2.0": { - "type": "package", - "compile": { - "lib/netstandard1.0/_._": {} - }, - "runtime": { - "lib/netstandard1.0/_._": {} - } - }, - "Microsoft.NETCore.Targets/2.0.0": { - "type": "package", - "compile": { - "lib/netstandard1.0/_._": {} - }, - "runtime": { - "lib/netstandard1.0/_._": {} - } - }, - "NETStandard.Library/2.0.3": { - "type": "package", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0" - }, - "compile": { - "lib/netstandard1.0/_._": {} - }, - "runtime": { - "lib/netstandard1.0/_._": {} - }, - "build": { - "build/netstandard2.0/NETStandard.Library.targets": {} - } - } - } - }, - "libraries": { - "Microsoft.NETCore.App/2.2.0": { - "sha512": "7z5l8Jp324S8bU8+yyWeYHXUFYvKyiI5lqS1dXgTzOx1H69Qbf6df12kCKlNX45LpMfCMd4U3M6p7Rl5Zk7SLA==", - "type": "package", - "path": "microsoft.netcore.app/2.2.0", - "files": [ - ".nupkg.metadata", - ".signature.p7s", - "LICENSE.TXT", - "Microsoft.NETCore.App.versions.txt", - "THIRD-PARTY-NOTICES.TXT", - "build/netcoreapp2.2/Microsoft.NETCore.App.PlatformManifest.txt", - "build/netcoreapp2.2/Microsoft.NETCore.App.props", - "build/netcoreapp2.2/Microsoft.NETCore.App.targets", - "microsoft.netcore.app.2.2.0.nupkg.sha512", - "microsoft.netcore.app.nuspec", - "ref/netcoreapp2.2/Microsoft.CSharp.dll", - "ref/netcoreapp2.2/Microsoft.CSharp.xml", - "ref/netcoreapp2.2/Microsoft.VisualBasic.dll", - "ref/netcoreapp2.2/Microsoft.VisualBasic.xml", - "ref/netcoreapp2.2/Microsoft.Win32.Primitives.dll", - "ref/netcoreapp2.2/Microsoft.Win32.Primitives.xml", - "ref/netcoreapp2.2/System.AppContext.dll", - "ref/netcoreapp2.2/System.Buffers.dll", - "ref/netcoreapp2.2/System.Buffers.xml", - "ref/netcoreapp2.2/System.Collections.Concurrent.dll", - "ref/netcoreapp2.2/System.Collections.Concurrent.xml", - "ref/netcoreapp2.2/System.Collections.Immutable.dll", - "ref/netcoreapp2.2/System.Collections.Immutable.xml", - "ref/netcoreapp2.2/System.Collections.NonGeneric.dll", - "ref/netcoreapp2.2/System.Collections.NonGeneric.xml", - "ref/netcoreapp2.2/System.Collections.Specialized.dll", - "ref/netcoreapp2.2/System.Collections.Specialized.xml", - "ref/netcoreapp2.2/System.Collections.dll", - "ref/netcoreapp2.2/System.Collections.xml", - "ref/netcoreapp2.2/System.ComponentModel.Annotations.dll", - "ref/netcoreapp2.2/System.ComponentModel.Annotations.xml", - "ref/netcoreapp2.2/System.ComponentModel.DataAnnotations.dll", - "ref/netcoreapp2.2/System.ComponentModel.EventBasedAsync.dll", - "ref/netcoreapp2.2/System.ComponentModel.EventBasedAsync.xml", - "ref/netcoreapp2.2/System.ComponentModel.Primitives.dll", - "ref/netcoreapp2.2/System.ComponentModel.Primitives.xml", - "ref/netcoreapp2.2/System.ComponentModel.TypeConverter.dll", - "ref/netcoreapp2.2/System.ComponentModel.TypeConverter.xml", - "ref/netcoreapp2.2/System.ComponentModel.dll", - "ref/netcoreapp2.2/System.ComponentModel.xml", - "ref/netcoreapp2.2/System.Configuration.dll", - "ref/netcoreapp2.2/System.Console.dll", - "ref/netcoreapp2.2/System.Console.xml", - "ref/netcoreapp2.2/System.Core.dll", - "ref/netcoreapp2.2/System.Data.Common.dll", - "ref/netcoreapp2.2/System.Data.Common.xml", - "ref/netcoreapp2.2/System.Data.dll", - "ref/netcoreapp2.2/System.Diagnostics.Contracts.dll", - "ref/netcoreapp2.2/System.Diagnostics.Contracts.xml", - "ref/netcoreapp2.2/System.Diagnostics.Debug.dll", - "ref/netcoreapp2.2/System.Diagnostics.Debug.xml", - "ref/netcoreapp2.2/System.Diagnostics.DiagnosticSource.dll", - "ref/netcoreapp2.2/System.Diagnostics.DiagnosticSource.xml", - "ref/netcoreapp2.2/System.Diagnostics.FileVersionInfo.dll", - "ref/netcoreapp2.2/System.Diagnostics.FileVersionInfo.xml", - "ref/netcoreapp2.2/System.Diagnostics.Process.dll", - "ref/netcoreapp2.2/System.Diagnostics.Process.xml", - "ref/netcoreapp2.2/System.Diagnostics.StackTrace.dll", - "ref/netcoreapp2.2/System.Diagnostics.StackTrace.xml", - "ref/netcoreapp2.2/System.Diagnostics.TextWriterTraceListener.dll", - "ref/netcoreapp2.2/System.Diagnostics.TextWriterTraceListener.xml", - "ref/netcoreapp2.2/System.Diagnostics.Tools.dll", - "ref/netcoreapp2.2/System.Diagnostics.Tools.xml", - "ref/netcoreapp2.2/System.Diagnostics.TraceSource.dll", - "ref/netcoreapp2.2/System.Diagnostics.TraceSource.xml", - "ref/netcoreapp2.2/System.Diagnostics.Tracing.dll", - "ref/netcoreapp2.2/System.Diagnostics.Tracing.xml", - "ref/netcoreapp2.2/System.Drawing.Primitives.dll", - "ref/netcoreapp2.2/System.Drawing.Primitives.xml", - "ref/netcoreapp2.2/System.Drawing.dll", - "ref/netcoreapp2.2/System.Dynamic.Runtime.dll", - "ref/netcoreapp2.2/System.Globalization.Calendars.dll", - "ref/netcoreapp2.2/System.Globalization.Extensions.dll", - "ref/netcoreapp2.2/System.Globalization.dll", - "ref/netcoreapp2.2/System.IO.Compression.Brotli.dll", - "ref/netcoreapp2.2/System.IO.Compression.FileSystem.dll", - "ref/netcoreapp2.2/System.IO.Compression.ZipFile.dll", - "ref/netcoreapp2.2/System.IO.Compression.ZipFile.xml", - "ref/netcoreapp2.2/System.IO.Compression.dll", - "ref/netcoreapp2.2/System.IO.Compression.xml", - "ref/netcoreapp2.2/System.IO.FileSystem.DriveInfo.dll", - "ref/netcoreapp2.2/System.IO.FileSystem.DriveInfo.xml", - "ref/netcoreapp2.2/System.IO.FileSystem.Primitives.dll", - "ref/netcoreapp2.2/System.IO.FileSystem.Watcher.dll", - "ref/netcoreapp2.2/System.IO.FileSystem.Watcher.xml", - "ref/netcoreapp2.2/System.IO.FileSystem.dll", - "ref/netcoreapp2.2/System.IO.FileSystem.xml", - "ref/netcoreapp2.2/System.IO.IsolatedStorage.dll", - "ref/netcoreapp2.2/System.IO.IsolatedStorage.xml", - "ref/netcoreapp2.2/System.IO.MemoryMappedFiles.dll", - "ref/netcoreapp2.2/System.IO.MemoryMappedFiles.xml", - "ref/netcoreapp2.2/System.IO.Pipes.dll", - "ref/netcoreapp2.2/System.IO.Pipes.xml", - "ref/netcoreapp2.2/System.IO.UnmanagedMemoryStream.dll", - "ref/netcoreapp2.2/System.IO.dll", - "ref/netcoreapp2.2/System.Linq.Expressions.dll", - "ref/netcoreapp2.2/System.Linq.Expressions.xml", - "ref/netcoreapp2.2/System.Linq.Parallel.dll", - "ref/netcoreapp2.2/System.Linq.Parallel.xml", - "ref/netcoreapp2.2/System.Linq.Queryable.dll", - "ref/netcoreapp2.2/System.Linq.Queryable.xml", - "ref/netcoreapp2.2/System.Linq.dll", - "ref/netcoreapp2.2/System.Linq.xml", - "ref/netcoreapp2.2/System.Memory.dll", - "ref/netcoreapp2.2/System.Memory.xml", - "ref/netcoreapp2.2/System.Net.Http.dll", - "ref/netcoreapp2.2/System.Net.Http.xml", - "ref/netcoreapp2.2/System.Net.HttpListener.dll", - "ref/netcoreapp2.2/System.Net.HttpListener.xml", - "ref/netcoreapp2.2/System.Net.Mail.dll", - "ref/netcoreapp2.2/System.Net.Mail.xml", - "ref/netcoreapp2.2/System.Net.NameResolution.dll", - "ref/netcoreapp2.2/System.Net.NameResolution.xml", - "ref/netcoreapp2.2/System.Net.NetworkInformation.dll", - "ref/netcoreapp2.2/System.Net.NetworkInformation.xml", - "ref/netcoreapp2.2/System.Net.Ping.dll", - "ref/netcoreapp2.2/System.Net.Ping.xml", - "ref/netcoreapp2.2/System.Net.Primitives.dll", - "ref/netcoreapp2.2/System.Net.Primitives.xml", - "ref/netcoreapp2.2/System.Net.Requests.dll", - "ref/netcoreapp2.2/System.Net.Requests.xml", - "ref/netcoreapp2.2/System.Net.Security.dll", - "ref/netcoreapp2.2/System.Net.Security.xml", - "ref/netcoreapp2.2/System.Net.ServicePoint.dll", - "ref/netcoreapp2.2/System.Net.ServicePoint.xml", - "ref/netcoreapp2.2/System.Net.Sockets.dll", - "ref/netcoreapp2.2/System.Net.Sockets.xml", - "ref/netcoreapp2.2/System.Net.WebClient.dll", - "ref/netcoreapp2.2/System.Net.WebClient.xml", - "ref/netcoreapp2.2/System.Net.WebHeaderCollection.dll", - "ref/netcoreapp2.2/System.Net.WebHeaderCollection.xml", - "ref/netcoreapp2.2/System.Net.WebProxy.dll", - "ref/netcoreapp2.2/System.Net.WebProxy.xml", - "ref/netcoreapp2.2/System.Net.WebSockets.Client.dll", - "ref/netcoreapp2.2/System.Net.WebSockets.Client.xml", - "ref/netcoreapp2.2/System.Net.WebSockets.dll", - "ref/netcoreapp2.2/System.Net.WebSockets.xml", - "ref/netcoreapp2.2/System.Net.dll", - "ref/netcoreapp2.2/System.Numerics.Vectors.dll", - "ref/netcoreapp2.2/System.Numerics.Vectors.xml", - "ref/netcoreapp2.2/System.Numerics.dll", - "ref/netcoreapp2.2/System.ObjectModel.dll", - "ref/netcoreapp2.2/System.ObjectModel.xml", - "ref/netcoreapp2.2/System.Reflection.DispatchProxy.dll", - "ref/netcoreapp2.2/System.Reflection.DispatchProxy.xml", - "ref/netcoreapp2.2/System.Reflection.Emit.ILGeneration.dll", - "ref/netcoreapp2.2/System.Reflection.Emit.ILGeneration.xml", - "ref/netcoreapp2.2/System.Reflection.Emit.Lightweight.dll", - "ref/netcoreapp2.2/System.Reflection.Emit.Lightweight.xml", - "ref/netcoreapp2.2/System.Reflection.Emit.dll", - "ref/netcoreapp2.2/System.Reflection.Emit.xml", - "ref/netcoreapp2.2/System.Reflection.Extensions.dll", - "ref/netcoreapp2.2/System.Reflection.Metadata.dll", - "ref/netcoreapp2.2/System.Reflection.Metadata.xml", - "ref/netcoreapp2.2/System.Reflection.Primitives.dll", - "ref/netcoreapp2.2/System.Reflection.Primitives.xml", - "ref/netcoreapp2.2/System.Reflection.TypeExtensions.dll", - "ref/netcoreapp2.2/System.Reflection.TypeExtensions.xml", - "ref/netcoreapp2.2/System.Reflection.dll", - "ref/netcoreapp2.2/System.Resources.Reader.dll", - "ref/netcoreapp2.2/System.Resources.ResourceManager.dll", - "ref/netcoreapp2.2/System.Resources.ResourceManager.xml", - "ref/netcoreapp2.2/System.Resources.Writer.dll", - "ref/netcoreapp2.2/System.Resources.Writer.xml", - "ref/netcoreapp2.2/System.Runtime.CompilerServices.VisualC.dll", - "ref/netcoreapp2.2/System.Runtime.CompilerServices.VisualC.xml", - "ref/netcoreapp2.2/System.Runtime.Extensions.dll", - "ref/netcoreapp2.2/System.Runtime.Extensions.xml", - "ref/netcoreapp2.2/System.Runtime.Handles.dll", - "ref/netcoreapp2.2/System.Runtime.InteropServices.RuntimeInformation.dll", - "ref/netcoreapp2.2/System.Runtime.InteropServices.RuntimeInformation.xml", - "ref/netcoreapp2.2/System.Runtime.InteropServices.WindowsRuntime.dll", - "ref/netcoreapp2.2/System.Runtime.InteropServices.WindowsRuntime.xml", - "ref/netcoreapp2.2/System.Runtime.InteropServices.dll", - "ref/netcoreapp2.2/System.Runtime.InteropServices.xml", - "ref/netcoreapp2.2/System.Runtime.Loader.dll", - "ref/netcoreapp2.2/System.Runtime.Loader.xml", - "ref/netcoreapp2.2/System.Runtime.Numerics.dll", - "ref/netcoreapp2.2/System.Runtime.Numerics.xml", - "ref/netcoreapp2.2/System.Runtime.Serialization.Formatters.dll", - "ref/netcoreapp2.2/System.Runtime.Serialization.Formatters.xml", - "ref/netcoreapp2.2/System.Runtime.Serialization.Json.dll", - "ref/netcoreapp2.2/System.Runtime.Serialization.Json.xml", - "ref/netcoreapp2.2/System.Runtime.Serialization.Primitives.dll", - "ref/netcoreapp2.2/System.Runtime.Serialization.Primitives.xml", - "ref/netcoreapp2.2/System.Runtime.Serialization.Xml.dll", - "ref/netcoreapp2.2/System.Runtime.Serialization.Xml.xml", - "ref/netcoreapp2.2/System.Runtime.Serialization.dll", - "ref/netcoreapp2.2/System.Runtime.dll", - "ref/netcoreapp2.2/System.Runtime.xml", - "ref/netcoreapp2.2/System.Security.Claims.dll", - "ref/netcoreapp2.2/System.Security.Claims.xml", - "ref/netcoreapp2.2/System.Security.Cryptography.Algorithms.dll", - "ref/netcoreapp2.2/System.Security.Cryptography.Algorithms.xml", - "ref/netcoreapp2.2/System.Security.Cryptography.Csp.dll", - "ref/netcoreapp2.2/System.Security.Cryptography.Csp.xml", - "ref/netcoreapp2.2/System.Security.Cryptography.Encoding.dll", - "ref/netcoreapp2.2/System.Security.Cryptography.Encoding.xml", - "ref/netcoreapp2.2/System.Security.Cryptography.Primitives.dll", - "ref/netcoreapp2.2/System.Security.Cryptography.Primitives.xml", - "ref/netcoreapp2.2/System.Security.Cryptography.X509Certificates.dll", - "ref/netcoreapp2.2/System.Security.Cryptography.X509Certificates.xml", - "ref/netcoreapp2.2/System.Security.Principal.dll", - "ref/netcoreapp2.2/System.Security.Principal.xml", - "ref/netcoreapp2.2/System.Security.SecureString.dll", - "ref/netcoreapp2.2/System.Security.dll", - "ref/netcoreapp2.2/System.ServiceModel.Web.dll", - "ref/netcoreapp2.2/System.ServiceProcess.dll", - "ref/netcoreapp2.2/System.Text.Encoding.Extensions.dll", - "ref/netcoreapp2.2/System.Text.Encoding.Extensions.xml", - "ref/netcoreapp2.2/System.Text.Encoding.dll", - "ref/netcoreapp2.2/System.Text.RegularExpressions.dll", - "ref/netcoreapp2.2/System.Text.RegularExpressions.xml", - "ref/netcoreapp2.2/System.Threading.Overlapped.dll", - "ref/netcoreapp2.2/System.Threading.Overlapped.xml", - "ref/netcoreapp2.2/System.Threading.Tasks.Dataflow.dll", - "ref/netcoreapp2.2/System.Threading.Tasks.Dataflow.xml", - "ref/netcoreapp2.2/System.Threading.Tasks.Extensions.dll", - "ref/netcoreapp2.2/System.Threading.Tasks.Extensions.xml", - "ref/netcoreapp2.2/System.Threading.Tasks.Parallel.dll", - "ref/netcoreapp2.2/System.Threading.Tasks.Parallel.xml", - "ref/netcoreapp2.2/System.Threading.Tasks.dll", - "ref/netcoreapp2.2/System.Threading.Tasks.xml", - "ref/netcoreapp2.2/System.Threading.Thread.dll", - "ref/netcoreapp2.2/System.Threading.Thread.xml", - "ref/netcoreapp2.2/System.Threading.ThreadPool.dll", - "ref/netcoreapp2.2/System.Threading.ThreadPool.xml", - "ref/netcoreapp2.2/System.Threading.Timer.dll", - "ref/netcoreapp2.2/System.Threading.Timer.xml", - "ref/netcoreapp2.2/System.Threading.dll", - "ref/netcoreapp2.2/System.Threading.xml", - "ref/netcoreapp2.2/System.Transactions.Local.dll", - "ref/netcoreapp2.2/System.Transactions.Local.xml", - "ref/netcoreapp2.2/System.Transactions.dll", - "ref/netcoreapp2.2/System.ValueTuple.dll", - "ref/netcoreapp2.2/System.Web.HttpUtility.dll", - "ref/netcoreapp2.2/System.Web.HttpUtility.xml", - "ref/netcoreapp2.2/System.Web.dll", - "ref/netcoreapp2.2/System.Windows.dll", - "ref/netcoreapp2.2/System.Xml.Linq.dll", - "ref/netcoreapp2.2/System.Xml.ReaderWriter.dll", - "ref/netcoreapp2.2/System.Xml.ReaderWriter.xml", - "ref/netcoreapp2.2/System.Xml.Serialization.dll", - "ref/netcoreapp2.2/System.Xml.XDocument.dll", - "ref/netcoreapp2.2/System.Xml.XDocument.xml", - "ref/netcoreapp2.2/System.Xml.XPath.XDocument.dll", - "ref/netcoreapp2.2/System.Xml.XPath.XDocument.xml", - "ref/netcoreapp2.2/System.Xml.XPath.dll", - "ref/netcoreapp2.2/System.Xml.XPath.xml", - "ref/netcoreapp2.2/System.Xml.XmlDocument.dll", - "ref/netcoreapp2.2/System.Xml.XmlSerializer.dll", - "ref/netcoreapp2.2/System.Xml.XmlSerializer.xml", - "ref/netcoreapp2.2/System.Xml.dll", - "ref/netcoreapp2.2/System.dll", - "ref/netcoreapp2.2/WindowsBase.dll", - "ref/netcoreapp2.2/mscorlib.dll", - "ref/netcoreapp2.2/netstandard.dll", - "runtime.json" - ] - }, - "Microsoft.NETCore.DotNetAppHost/2.2.0": { - "sha512": "DrhaKInRKKvN6Ns2VNIlC7ZffLOp9THf8cO6X4fytPRJovJUbF49/zzx4WfgX9E44FMsw9hT8hrKiIqDSHvGvA==", - "type": "package", - "path": "microsoft.netcore.dotnetapphost/2.2.0", - "files": [ - ".nupkg.metadata", - ".signature.p7s", - "LICENSE.TXT", - "THIRD-PARTY-NOTICES.TXT", - "microsoft.netcore.dotnetapphost.2.2.0.nupkg.sha512", - "microsoft.netcore.dotnetapphost.nuspec", - "runtime.json" - ] - }, - "Microsoft.NETCore.DotNetHostPolicy/2.2.0": { - "sha512": "FJie7IoPZFaPgNDxhZGmDBQP/Bs5vPdfca/G2Wf9gd6LIvMYkZcibtmJwB4tcf4KXkaOYfIOo4Cl9sEPMsSzkw==", - "type": "package", - "path": "microsoft.netcore.dotnethostpolicy/2.2.0", - "files": [ - ".nupkg.metadata", - ".signature.p7s", - "LICENSE.TXT", - "THIRD-PARTY-NOTICES.TXT", - "microsoft.netcore.dotnethostpolicy.2.2.0.nupkg.sha512", - "microsoft.netcore.dotnethostpolicy.nuspec", - "runtime.json" - ] - }, - "Microsoft.NETCore.DotNetHostResolver/2.2.0": { - "sha512": "spDm3AJYmebthDNhzY17YLPtvbc+Y1lCLVeiIH1uLJ/hZaM+40pBiPefFR8J1u66Ndkqi8ipR2tEbqPnYnjRhw==", - "type": "package", - "path": "microsoft.netcore.dotnethostresolver/2.2.0", - "files": [ - ".nupkg.metadata", - ".signature.p7s", - "LICENSE.TXT", - "THIRD-PARTY-NOTICES.TXT", - "microsoft.netcore.dotnethostresolver.2.2.0.nupkg.sha512", - "microsoft.netcore.dotnethostresolver.nuspec", - "runtime.json" - ] - }, - "Microsoft.NETCore.Platforms/2.2.0": { - "sha512": "T/J+XZo+YheFTJh8/4uoeJDdz5qOmOMkjg6/VL8mHJ9AnP8+fmV/kcbxeXsob0irRNiChf+V0ig1MCRLp/+Kog==", - "type": "package", - "path": "microsoft.netcore.platforms/2.2.0", - "files": [ - ".nupkg.metadata", - ".signature.p7s", - "LICENSE.TXT", - "THIRD-PARTY-NOTICES.TXT", - "lib/netstandard1.0/_._", - "microsoft.netcore.platforms.2.2.0.nupkg.sha512", - "microsoft.netcore.platforms.nuspec", - "runtime.json", - "useSharedDesignerContext.txt", - "version.txt" - ] - }, - "Microsoft.NETCore.Targets/2.0.0": { - "sha512": "odP/tJj1z6GylFpNo7pMtbd/xQgTC3Ex2If63dRTL38bBNMwsBnJ+RceUIyHdRBC0oik/3NehYT+oECwBhIM3Q==", - "type": "package", - "path": "microsoft.netcore.targets/2.0.0", - "files": [ - ".nupkg.metadata", - "LICENSE.TXT", - "THIRD-PARTY-NOTICES.TXT", - "lib/netstandard1.0/_._", - "microsoft.netcore.targets.2.0.0.nupkg.sha512", - "microsoft.netcore.targets.nuspec", - "runtime.json", - "useSharedDesignerContext.txt", - "version.txt" - ] - }, - "NETStandard.Library/2.0.3": { - "sha512": "st47PosZSHrjECdjeIzZQbzivYBJFv6P2nv4cj2ypdI204DO+vZ7l5raGMiX4eXMJ53RfOIg+/s4DHVZ54Nu2A==", - "type": "package", - "path": "netstandard.library/2.0.3", - "files": [ - ".nupkg.metadata", - "LICENSE.TXT", - "THIRD-PARTY-NOTICES.TXT", - "build/netstandard2.0/NETStandard.Library.targets", - "build/netstandard2.0/ref/Microsoft.Win32.Primitives.dll", - "build/netstandard2.0/ref/System.AppContext.dll", - "build/netstandard2.0/ref/System.Collections.Concurrent.dll", - "build/netstandard2.0/ref/System.Collections.NonGeneric.dll", - "build/netstandard2.0/ref/System.Collections.Specialized.dll", - "build/netstandard2.0/ref/System.Collections.dll", - "build/netstandard2.0/ref/System.ComponentModel.Composition.dll", - "build/netstandard2.0/ref/System.ComponentModel.EventBasedAsync.dll", - "build/netstandard2.0/ref/System.ComponentModel.Primitives.dll", - "build/netstandard2.0/ref/System.ComponentModel.TypeConverter.dll", - "build/netstandard2.0/ref/System.ComponentModel.dll", - "build/netstandard2.0/ref/System.Console.dll", - "build/netstandard2.0/ref/System.Core.dll", - "build/netstandard2.0/ref/System.Data.Common.dll", - "build/netstandard2.0/ref/System.Data.dll", - "build/netstandard2.0/ref/System.Diagnostics.Contracts.dll", - "build/netstandard2.0/ref/System.Diagnostics.Debug.dll", - "build/netstandard2.0/ref/System.Diagnostics.FileVersionInfo.dll", - "build/netstandard2.0/ref/System.Diagnostics.Process.dll", - "build/netstandard2.0/ref/System.Diagnostics.StackTrace.dll", - "build/netstandard2.0/ref/System.Diagnostics.TextWriterTraceListener.dll", - "build/netstandard2.0/ref/System.Diagnostics.Tools.dll", - "build/netstandard2.0/ref/System.Diagnostics.TraceSource.dll", - "build/netstandard2.0/ref/System.Diagnostics.Tracing.dll", - "build/netstandard2.0/ref/System.Drawing.Primitives.dll", - "build/netstandard2.0/ref/System.Drawing.dll", - "build/netstandard2.0/ref/System.Dynamic.Runtime.dll", - "build/netstandard2.0/ref/System.Globalization.Calendars.dll", - "build/netstandard2.0/ref/System.Globalization.Extensions.dll", - "build/netstandard2.0/ref/System.Globalization.dll", - "build/netstandard2.0/ref/System.IO.Compression.FileSystem.dll", - "build/netstandard2.0/ref/System.IO.Compression.ZipFile.dll", - "build/netstandard2.0/ref/System.IO.Compression.dll", - "build/netstandard2.0/ref/System.IO.FileSystem.DriveInfo.dll", - "build/netstandard2.0/ref/System.IO.FileSystem.Primitives.dll", - "build/netstandard2.0/ref/System.IO.FileSystem.Watcher.dll", - "build/netstandard2.0/ref/System.IO.FileSystem.dll", - "build/netstandard2.0/ref/System.IO.IsolatedStorage.dll", - "build/netstandard2.0/ref/System.IO.MemoryMappedFiles.dll", - "build/netstandard2.0/ref/System.IO.Pipes.dll", - "build/netstandard2.0/ref/System.IO.UnmanagedMemoryStream.dll", - "build/netstandard2.0/ref/System.IO.dll", - "build/netstandard2.0/ref/System.Linq.Expressions.dll", - "build/netstandard2.0/ref/System.Linq.Parallel.dll", - "build/netstandard2.0/ref/System.Linq.Queryable.dll", - "build/netstandard2.0/ref/System.Linq.dll", - "build/netstandard2.0/ref/System.Net.Http.dll", - "build/netstandard2.0/ref/System.Net.NameResolution.dll", - "build/netstandard2.0/ref/System.Net.NetworkInformation.dll", - "build/netstandard2.0/ref/System.Net.Ping.dll", - "build/netstandard2.0/ref/System.Net.Primitives.dll", - "build/netstandard2.0/ref/System.Net.Requests.dll", - "build/netstandard2.0/ref/System.Net.Security.dll", - "build/netstandard2.0/ref/System.Net.Sockets.dll", - "build/netstandard2.0/ref/System.Net.WebHeaderCollection.dll", - "build/netstandard2.0/ref/System.Net.WebSockets.Client.dll", - "build/netstandard2.0/ref/System.Net.WebSockets.dll", - "build/netstandard2.0/ref/System.Net.dll", - "build/netstandard2.0/ref/System.Numerics.dll", - "build/netstandard2.0/ref/System.ObjectModel.dll", - "build/netstandard2.0/ref/System.Reflection.Extensions.dll", - "build/netstandard2.0/ref/System.Reflection.Primitives.dll", - "build/netstandard2.0/ref/System.Reflection.dll", - "build/netstandard2.0/ref/System.Resources.Reader.dll", - "build/netstandard2.0/ref/System.Resources.ResourceManager.dll", - "build/netstandard2.0/ref/System.Resources.Writer.dll", - "build/netstandard2.0/ref/System.Runtime.CompilerServices.VisualC.dll", - "build/netstandard2.0/ref/System.Runtime.Extensions.dll", - "build/netstandard2.0/ref/System.Runtime.Handles.dll", - "build/netstandard2.0/ref/System.Runtime.InteropServices.RuntimeInformation.dll", - "build/netstandard2.0/ref/System.Runtime.InteropServices.dll", - "build/netstandard2.0/ref/System.Runtime.Numerics.dll", - "build/netstandard2.0/ref/System.Runtime.Serialization.Formatters.dll", - "build/netstandard2.0/ref/System.Runtime.Serialization.Json.dll", - "build/netstandard2.0/ref/System.Runtime.Serialization.Primitives.dll", - "build/netstandard2.0/ref/System.Runtime.Serialization.Xml.dll", - "build/netstandard2.0/ref/System.Runtime.Serialization.dll", - "build/netstandard2.0/ref/System.Runtime.dll", - "build/netstandard2.0/ref/System.Security.Claims.dll", - "build/netstandard2.0/ref/System.Security.Cryptography.Algorithms.dll", - "build/netstandard2.0/ref/System.Security.Cryptography.Csp.dll", - "build/netstandard2.0/ref/System.Security.Cryptography.Encoding.dll", - "build/netstandard2.0/ref/System.Security.Cryptography.Primitives.dll", - "build/netstandard2.0/ref/System.Security.Cryptography.X509Certificates.dll", - "build/netstandard2.0/ref/System.Security.Principal.dll", - "build/netstandard2.0/ref/System.Security.SecureString.dll", - "build/netstandard2.0/ref/System.ServiceModel.Web.dll", - "build/netstandard2.0/ref/System.Text.Encoding.Extensions.dll", - "build/netstandard2.0/ref/System.Text.Encoding.dll", - "build/netstandard2.0/ref/System.Text.RegularExpressions.dll", - "build/netstandard2.0/ref/System.Threading.Overlapped.dll", - "build/netstandard2.0/ref/System.Threading.Tasks.Parallel.dll", - "build/netstandard2.0/ref/System.Threading.Tasks.dll", - "build/netstandard2.0/ref/System.Threading.Thread.dll", - "build/netstandard2.0/ref/System.Threading.ThreadPool.dll", - "build/netstandard2.0/ref/System.Threading.Timer.dll", - "build/netstandard2.0/ref/System.Threading.dll", - "build/netstandard2.0/ref/System.Transactions.dll", - "build/netstandard2.0/ref/System.ValueTuple.dll", - "build/netstandard2.0/ref/System.Web.dll", - "build/netstandard2.0/ref/System.Windows.dll", - "build/netstandard2.0/ref/System.Xml.Linq.dll", - "build/netstandard2.0/ref/System.Xml.ReaderWriter.dll", - "build/netstandard2.0/ref/System.Xml.Serialization.dll", - "build/netstandard2.0/ref/System.Xml.XDocument.dll", - "build/netstandard2.0/ref/System.Xml.XPath.XDocument.dll", - "build/netstandard2.0/ref/System.Xml.XPath.dll", - "build/netstandard2.0/ref/System.Xml.XmlDocument.dll", - "build/netstandard2.0/ref/System.Xml.XmlSerializer.dll", - "build/netstandard2.0/ref/System.Xml.dll", - "build/netstandard2.0/ref/System.dll", - "build/netstandard2.0/ref/mscorlib.dll", - "build/netstandard2.0/ref/netstandard.dll", - "build/netstandard2.0/ref/netstandard.xml", - "lib/netstandard1.0/_._", - "netstandard.library.2.0.3.nupkg.sha512", - "netstandard.library.nuspec" - ] - } - }, - "projectFileDependencyGroups": { - ".NETCoreApp,Version=v2.2": [ - "Microsoft.NETCore.App >= 2.2.0" - ] - }, - "packageFolders": { - "/Users/ben/.nuget/packages/": {}, - "/usr/local/share/dotnet/sdk/NuGetFallbackFolder": {} - }, - "project": { - "version": "1.0.0", - "restore": { - "projectUniqueName": "/Users/ben/.vim/bundle/vimspector/support/test/csharp/csharp.csproj", - "projectName": "csharp", - "projectPath": "/Users/ben/.vim/bundle/vimspector/support/test/csharp/csharp.csproj", - "packagesPath": "/Users/ben/.nuget/packages/", - "outputPath": "/Users/ben/.vim/bundle/vimspector/support/test/csharp/obj/", - "projectStyle": "PackageReference", - "fallbackFolders": [ - "/usr/local/share/dotnet/sdk/NuGetFallbackFolder" - ], - "configFilePaths": [ - "/Users/ben/.nuget/NuGet/NuGet.Config" - ], - "originalTargetFrameworks": [ - "netcoreapp2.2" - ], - "sources": { - "https://api.nuget.org/v3/index.json": {} - }, - "frameworks": { - "netcoreapp2.2": { - "projectReferences": {} - } - }, - "warningProperties": { - "warnAsError": [ - "NU1605" - ] - } - }, - "frameworks": { - "netcoreapp2.2": { - "dependencies": { - "Microsoft.NETCore.App": { - "suppressParent": "All", - "target": "Package", - "version": "[2.2.0, )", - "autoReferenced": true - } - }, - "imports": [ - "net461" - ], - "assetTargetFallback": true, - "warn": true - } - } - } -} \ No newline at end of file diff --git a/support/test/example/attach.vim b/support/test/example/attach.vim new file mode 100644 index 0000000..dda00a9 --- /dev/null +++ b/support/test/example/attach.vim @@ -0,0 +1,13 @@ +if argc() < 2 + echom 'Usage:' v:argv[ 0 ] 'processName binary' + cquit! +endif + +setfiletype cpp +call vimspector#LaunchWithSettings( #{ + \ configuration: "C++ - Attach Local Process", + \ processName: argv( 0 ), + \ binary: argv( 1 ), + \ } ) + +1,2argd diff --git a/support/test/example/cpp.json b/support/test/example/cpp.json new file mode 100644 index 0000000..ba52fcd --- /dev/null +++ b/support/test/example/cpp.json @@ -0,0 +1,24 @@ +{ + "configurations": { + "C++ - Attach Local Process": { + "adapter": "vscode-cpptools", + "variables": { + "PID": { + "shell": [ "GetPIDForProcess", "${processName}" ] + } + }, + "configuration": { + "name": "test", + "request": "attach", + "program": "${binary}", + "processId": "${PID}", + + "type": "cppdbg", + "stopAtEntry": true, + "setupCommands": [ + { "text": "source ${initFile}", "ignoreFailures": true } + ] + } + } + } +} diff --git a/support/test/go/hello_world/.vimspector.json b/support/test/go/hello_world/.vimspector.json index 7934b48..4613b98 100644 --- a/support/test/go/hello_world/.vimspector.json +++ b/support/test/go/hello_world/.vimspector.json @@ -1,13 +1,58 @@ { + "adapters": { + "dlv-dap": { + "variables": { + "port": "${unusedLocalPort}" + }, + "command": [ + "$HOME/go/bin/dlv", + "dap", + "--listen", + "127.0.0.1:${port}" + ], + "port": "${port}" + } + }, "configurations": { "run": { "adapter": "vscode-go", + "default": true, "configuration": { "request": "launch", "program": "${workspaceRoot}/hello-world.go", "mode": "debug", "dlvToolPath": "$HOME/go/bin/dlv", - "trace": true + "trace": true, + "env": { "GO111MODULE": "off" } + } + }, + "run-dap": { + "adapter": "dlv-dap", + "configuration": { + "request": "launch", + "env": { "GO111MODULE": "off" }, + + "mode": "debug", // debug|test + "program": "${workspaceRoot}/hello-world.go" + + // "args": [], + // "buildFlags": ... + // "stackTraceDepth": ..., + // "showGlobalVariables": true, + } + }, + "run-exec": { + // NOTE: To use this you _must_ disable optimistaion: + // go build -o hello_world -gcflags="all=-N -l" + // https://github.com/golang/vscode-go/blob/master/docs/debugging.md#troubleshooting + "adapter": "vscode-go", + "configuration": { + "request": "launch", + "program": "${workspaceRoot}/hello-world", + "mode": "exec", + "dlvToolPath": "$HOME/go/bin/dlv", + "trace": true, + "env": { "GO111MODULE": "off" } } } } diff --git a/support/test/go/name-starts-with-vowel/.vimspector.json b/support/test/go/name-starts-with-vowel/.vimspector.json new file mode 100644 index 0000000..ffcfc93 --- /dev/null +++ b/support/test/go/name-starts-with-vowel/.vimspector.json @@ -0,0 +1,29 @@ +{ + "configurations": { + "run-cmd": { + "adapter": "vscode-go", + "configuration": { + "request": "launch", + "program": "${workspaceRoot}/cmd/namestartswithvowel/main.go", + "mode": "debug", + "dlvToolPath": "$HOME/go/bin/dlv", + "dlvLoadConfig": { + "maxArrayValues": 1000, + "maxStringLen": 1000 + } + } + }, + "test-current-file": { + "adapter": "vscode-go", + "configuration": { + "request": "launch", + "mode": "test", + "program": "${fileDirname}", + "cwd": "${fileDirname}", + "dlvToolPath": "$GOPATH/bin/dlv", + "env": {}, + "args": [] + } + } + } +} diff --git a/support/test/go/name-starts-with-vowel/README.md b/support/test/go/name-starts-with-vowel/README.md new file mode 100644 index 0000000..fec967e --- /dev/null +++ b/support/test/go/name-starts-with-vowel/README.md @@ -0,0 +1,33 @@ +# Purpose + +This example comes with two example vimspector configs for the Go programming language. + +1) `run-cmd` will launch the main programme under `cmd/namestartswithvowel`. +1) `test-current-file` will run the tests in the current file in debug mode. + +## Example use-cases + +### run-cmd + +* Open `cmd/namestartswithvowel/main.go` +* Add a breakpoint somewhere within the programme +* Start the debugger (`:call vimspector#Continue()` or your relevant keymapping) +* Select the first launch configuration (`1: run-cmd`) + +### test-current-file + +* Open `internal/vowels/vowels_test.go` +* Add a breakpoint somewhere within the test +* Start the debugger (`:call vimspector#Continue()` or your relevant keymapping) +* Select the second launch configuration (`2: test-current-file`) + +## Additional Configuration + +There are two additional configuration options specified under `run-cmd`; these parameters configure the maximum string/array size to be shown while debugging. + +``` +"dlvLoadConfig": { + "maxArrayValues": 1000, + "maxStringLen": 1000 +} +``` diff --git a/support/test/go/name-starts-with-vowel/cmd/namestartswithvowel/main.go b/support/test/go/name-starts-with-vowel/cmd/namestartswithvowel/main.go new file mode 100644 index 0000000..c160aea --- /dev/null +++ b/support/test/go/name-starts-with-vowel/cmd/namestartswithvowel/main.go @@ -0,0 +1,20 @@ +package main + +import ( + "fmt" + + "example.com/internal/vowels" +) + +func main() { + names := []string{"Simon", "Bob", "Jennifer", "Amy", "Duke", "Elizabeth"} + + for _, n := range names { + if vowels.NameStartsWithVowel(n) { + fmt.Printf("%s starts with a vowel!\n", n) + continue + } + + fmt.Printf("%s does not start with a vowel!\n", n) + } +} diff --git a/support/test/go/name-starts-with-vowel/go.mod b/support/test/go/name-starts-with-vowel/go.mod new file mode 100644 index 0000000..3070734 --- /dev/null +++ b/support/test/go/name-starts-with-vowel/go.mod @@ -0,0 +1,3 @@ +module example.com + +go 1.16 diff --git a/support/test/go/name-starts-with-vowel/internal/vowels/vowels.go b/support/test/go/name-starts-with-vowel/internal/vowels/vowels.go new file mode 100644 index 0000000..4e76480 --- /dev/null +++ b/support/test/go/name-starts-with-vowel/internal/vowels/vowels.go @@ -0,0 +1,9 @@ +package vowels + +import "strings" + +func NameStartsWithVowel(name string) bool { + s := strings.Split(strings.ToLower(name), "") + + return s[0] == "a" || s[0] == "e" || s[0] == "i" || s[0] == "o" || s[0] == "u" +} diff --git a/support/test/go/name-starts-with-vowel/internal/vowels/vowels_test.go b/support/test/go/name-starts-with-vowel/internal/vowels/vowels_test.go new file mode 100644 index 0000000..e0d5773 --- /dev/null +++ b/support/test/go/name-starts-with-vowel/internal/vowels/vowels_test.go @@ -0,0 +1,30 @@ +package vowels + +import ( + "fmt" + "testing" +) + +func TestNameStartsWithVowel(t *testing.T) { + testCases := []struct { + input string + expectedOutput bool + }{ + { + input: "Simon", + expectedOutput: false, + }, + { + input: "Andy", + expectedOutput: true, + }, + } + for _, tt := range testCases { + t.Run(fmt.Sprintf("%s should product %t", tt.input, tt.expectedOutput), func(t *testing.T) { + out := NameStartsWithVowel(tt.input) + if out != tt.expectedOutput { + t.Errorf("%s produced %t, when %t was expected", tt.input, out, tt.expectedOutput) + } + }) + } +} diff --git a/support/test/java/test_project/.vimspector.json b/support/test/java/test_project/.vimspector.json index 43b07aa..39f4639 100644 --- a/support/test/java/test_project/.vimspector.json +++ b/support/test/java/test_project/.vimspector.json @@ -1,13 +1,7 @@ { - "adapters": { - "java-debug-server": { - "name": "vscode-java", - "port": "ask" - } - }, "configurations": { "Java Launch": { - "adapter": "java-debug-server", + "adapter": "vscode-java", "configuration": { "request": "launch", "mainClass": "com.vimspector.test.TestApplication", @@ -15,7 +9,33 @@ "classPaths": [ "${workspaceRoot}/target/classes" ], "args": "hello world!", "stopOnEntry": true, - "console": "integratedTerminal" + "console": "integratedTerminal", + "stepFilters": { + "skipClasses": [ "$$JDK" ] + } + } + }, + "Java Attach": { + "adapter": "vscode-java", + "configuration": { + "request": "attach", + "sourcePaths": [ "${workspaceRoot}/src/main/java" ], + "stopOnEntry": true, + "hostName": "localhost", + "port": "${JVMDebugPort}", + "stepFilters": { + "skipClasses": [ "$$JDK" ] + } + } + }, + "Attach with vscode-javac": { + "adapter": "vscode-javac", + "configuration": { + "request": "attach", + "port": "${debugPort}", + "sourceRoots": [ + "${workspaceRoot}/src/main/java" + ] } } } diff --git a/support/test/java/test_project/java.vim b/support/test/java/test_project/java.vim new file mode 100644 index 0000000..96b78d8 --- /dev/null +++ b/support/test/java/test_project/java.vim @@ -0,0 +1,24 @@ +let g:ycm_java_jdtls_extension_path = [ + \ expand( ':p:h:h:h:h:h' ) . '/gadgets/macos' + \ ] + +let s:jdt_ls_debugger_port = 0 +function! s:StartDebugging() + if s:jdt_ls_debugger_port <= 0 + " Get the DAP port + let s:jdt_ls_debugger_port = youcompleteme#GetCommandResponse( + \ 'ExecuteCommand', + \ 'vscode.java.startDebugSession' ) + + if s:jdt_ls_debugger_port == '' + echom "Unable to get DAP port - is YCM initialized?" + let s:jdt_ls_debugger_port = 0 + return + endif + endif + + " Start debugging with the DAP port + call vimspector#LaunchWithSettings( { 'DAPPort': s:jdt_ls_debugger_port } ) +endfunction + +nnoremap :call StartDebugging() diff --git a/support/test/java/test_project/pom.xml b/support/test/java/test_project/pom.xml index 10f207c..e6dc4d3 100644 --- a/support/test/java/test_project/pom.xml +++ b/support/test/java/test_project/pom.xml @@ -4,7 +4,7 @@ TestApplication 1 - 1.8 - 1.8 + 11 + 11 diff --git a/support/test/java/test_project/src/main/java/com/vimspector/test/Base.java b/support/test/java/test_project/src/main/java/com/vimspector/test/Base.java new file mode 100644 index 0000000..1cd89e5 --- /dev/null +++ b/support/test/java/test_project/src/main/java/com/vimspector/test/Base.java @@ -0,0 +1,11 @@ +package com.vimspector.test; + +public class Base +{ + public String DoSomething() + { + String s = new String(); + s.replace( "A", "B" ); + return s; + } +} diff --git a/support/test/java/test_project/src/main/java/com/vimspector/test/TestApplication.java b/support/test/java/test_project/src/main/java/com/vimspector/test/TestApplication.java index 17f9c96..264c0e6 100644 --- a/support/test/java/test_project/src/main/java/com/vimspector/test/TestApplication.java +++ b/support/test/java/test_project/src/main/java/com/vimspector/test/TestApplication.java @@ -13,6 +13,22 @@ public class TestApplication { return list; } + private static class Bass extends Base { + String bass = "Pump"; + @Override + public String DoSomething() { + if ( Math.random() % 3 == 0 ) { + return bass; + } + return super.DoSomething(); + } + } + + private static void DoGeneric( T b ) { + TestGeneric foo = new TestGeneric<>( b ); + foo.DoSomethingUseful(); + } + public static void main( String[] args ) { int numEntries = 0; for ( String s : args ) { @@ -24,6 +40,8 @@ public class TestApplication { ++numEntries; } System.out.println( "Number of entries: " + numEntries ); + + DoGeneric( new Bass() ); } } diff --git a/support/test/java/test_project/src/main/java/com/vimspector/test/TestGeneric.java b/support/test/java/test_project/src/main/java/com/vimspector/test/TestGeneric.java new file mode 100644 index 0000000..6cd5e01 --- /dev/null +++ b/support/test/java/test_project/src/main/java/com/vimspector/test/TestGeneric.java @@ -0,0 +1,16 @@ +package com.vimspector.test; + +class TestGeneric { + T base; + + public TestGeneric( T b ) + { + this.base = b; + } + + public String DoSomethingUseful() { + String s = "A B C" + base.DoSomething(); + + return s.replace( "B", "C" ); + } +} diff --git a/support/test/kotlin/.gitignore b/support/test/kotlin/.gitignore new file mode 100644 index 0000000..f8b92c3 --- /dev/null +++ b/support/test/kotlin/.gitignore @@ -0,0 +1,2 @@ +.gradle +build diff --git a/support/test/kotlin/.vimspector.json b/support/test/kotlin/.vimspector.json new file mode 100644 index 0000000..d2c2a63 --- /dev/null +++ b/support/test/kotlin/.vimspector.json @@ -0,0 +1,21 @@ +{ + "configurations": { + "kotlin-debug-adapter launch": { + "adapter": "cust_kotlin-debug-adapter", + "configuration": { + "request": "launch", + "projectRoot": "${workspaceFolder}", + "mainClass": "vimspector/test/ApplicationKt" + } + }, + "kotlin-debug-adapter attach": { + "adapter": "cust_kotlin-debug-adapter", + "configuration": { + "request": "attach", + "projectRoot": "${workspaceFolder}", + "hostName": "${hostName}", + "port": "${port}" + } + } + } +} diff --git a/support/test/kotlin/build.gradle.kts b/support/test/kotlin/build.gradle.kts new file mode 100644 index 0000000..e93c004 --- /dev/null +++ b/support/test/kotlin/build.gradle.kts @@ -0,0 +1,25 @@ +plugins { + kotlin("jvm") version "1.4.0" + + application +} + +repositories { + // Use jcenter for resolving dependencies. + // You can declare any Maven/Ivy/file repository here. + jcenter() +} + +dependencies { + // Align versions of all Kotlin components + implementation(platform("org.jetbrains.kotlin:kotlin-bom")) + + // Use the Kotlin JDK 8 standard library. + implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") +} + +application { + // Define the main class for the application. + mainClassName = "vimspector.test.ApplicationKt" +} + diff --git a/support/test/kotlin/settings.gradle.kts b/support/test/kotlin/settings.gradle.kts new file mode 100644 index 0000000..8cbc5be --- /dev/null +++ b/support/test/kotlin/settings.gradle.kts @@ -0,0 +1 @@ +rootProject.name = "vimspector-test" diff --git a/support/test/kotlin/src/main/kotlin/vimspector/test/Application.kt b/support/test/kotlin/src/main/kotlin/vimspector/test/Application.kt new file mode 100644 index 0000000..99655da --- /dev/null +++ b/support/test/kotlin/src/main/kotlin/vimspector/test/Application.kt @@ -0,0 +1,5 @@ +package vimspector.test + +fun main(args: Array) { + println("Hello World!") +} diff --git a/support/test/lua/love-headless/.vimspector.json b/support/test/lua/love-headless/.vimspector.json new file mode 100644 index 0000000..5e2cb99 --- /dev/null +++ b/support/test/lua/love-headless/.vimspector.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://puremourning.github.io/vimspector/schema/vimspector.schema.json#", + "configurations": { + "love": { + "adapter": "lua-local", + "configuration": { + "request": "launch", + "type": "lua-local", + "cwd": "${workspaceFolder}", + "program": { + "command": "love" + }, + "args": ["${workspaceFolder}"], + "stopOnEntry": false + } + } + } +} diff --git a/support/test/lua/love-headless/conf.lua b/support/test/lua/love-headless/conf.lua new file mode 100644 index 0000000..f74d620 --- /dev/null +++ b/support/test/lua/love-headless/conf.lua @@ -0,0 +1,12 @@ +function love.conf(t) + t.modules.audio = false + t.modules.font = false + t.modules.graphics = false + t.modules.image = false + t.modules.mouse = false + t.modules.physics = false + t.modules.sound = false + t.modules.touch = false + t.modules.video = false + t.modules.window = false +end diff --git a/support/test/lua/love-headless/main.lua b/support/test/lua/love-headless/main.lua new file mode 100644 index 0000000..899a864 --- /dev/null +++ b/support/test/lua/love-headless/main.lua @@ -0,0 +1,10 @@ +if pcall(require, 'lldebugger') then + require('lldebugger').start() +end + +local time = 0 + +function love.update(dt) + time = time + dt + love.event.quit() +end diff --git a/support/test/lua/love/.vimspector.json b/support/test/lua/love/.vimspector.json new file mode 100644 index 0000000..5e2cb99 --- /dev/null +++ b/support/test/lua/love/.vimspector.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://puremourning.github.io/vimspector/schema/vimspector.schema.json#", + "configurations": { + "love": { + "adapter": "lua-local", + "configuration": { + "request": "launch", + "type": "lua-local", + "cwd": "${workspaceFolder}", + "program": { + "command": "love" + }, + "args": ["${workspaceFolder}"], + "stopOnEntry": false + } + } + } +} diff --git a/support/test/lua/love/main.lua b/support/test/lua/love/main.lua new file mode 100644 index 0000000..0e67799 --- /dev/null +++ b/support/test/lua/love/main.lua @@ -0,0 +1,21 @@ +if pcall(require, 'lldebugger') then + require('lldebugger').start() +end + +local rect = {0, 0, 64, 64} + + +function love.update(dt) + rect[1] = rect[1] + 10 * dt + rect[2] = rect[2] + 10 * dt +end + + +function love.draw() + love.graphics.rectangle('fill', rect[1], rect[2], rect[3], rect[4]) +end + + +function love.keypressed() + love.event.quit() +end diff --git a/support/test/lua/simple/.vimspector.json b/support/test/lua/simple/.vimspector.json new file mode 100644 index 0000000..10d39dc --- /dev/null +++ b/support/test/lua/simple/.vimspector.json @@ -0,0 +1,31 @@ +{ + "$schema": "https://puremourning.github.io/vimspector/schema/vimspector.schema.json#", + "configurations": { + "lua": { + "adapter": "lua-local", + "configuration": { + "request": "launch", + "type": "lua-local", + "cwd": "${workspaceFolder}", + "program": { + "lua": "lua", + "file": "simple.lua", + "stopOnEntry": false + } + } + }, + "luajit": { + "adapter": "lua-local", + "configuration": { + "request": "launch", + "type": "lua-local", + "cwd": "${workspaceFolder}", + "program": { + "lua": "luajit", + "file": "simple.lua", + "stopOnEntry": false + } + } + } + } +} diff --git a/support/test/lua/simple/simple.lua b/support/test/lua/simple/simple.lua new file mode 100644 index 0000000..ed74655 --- /dev/null +++ b/support/test/lua/simple/simple.lua @@ -0,0 +1,21 @@ +local separator = ' ' + + +local function createMessage() + local words = {} + table.insert(words, 'Hello') + table.insert(words, 'world') + return table.concat(words, separator) +end + + +local function withEmphasis(func) + return function() + return func() .. '!' + end +end + + +createMessage = withEmphasis(createMessage) + +print(createMessage()) diff --git a/support/test/node/simple/simple.js b/support/test/node/simple/simple.js index fc6e558..f5c51f4 100644 --- a/support/test/node/simple/simple.js +++ b/support/test/node/simple/simple.js @@ -1,3 +1,10 @@ var msg = 'Hello, world!' -console.log( "OK stuff happened" ) +var obj = { + test: 'testing', + toast: function() { + return 'toasty' + this.test; + } +} + +console.log( "OK stuff happened " + obj.toast() ) diff --git a/support/test/python/multiple_files/.vimspector.json b/support/test/python/multiple_files/.vimspector.json new file mode 100644 index 0000000..aff2cb6 --- /dev/null +++ b/support/test/python/multiple_files/.vimspector.json @@ -0,0 +1,21 @@ +{ + "configurations": { + "run": { + "adapter": "debugpy", + "default": true, + "configuration": { + "request": "launch", + "program": "${workspaceRoot}/moo.py", + "cwd": "${workspaceRoot}", + "stopOnEntry": true + }, + "breakpoints": { + "exception": { + "raised": "N", + "uncaught": "", + "userUnhandled": "" + } + } + } + } +} diff --git a/support/test/python/multiple_files/cow.py b/support/test/python/multiple_files/cow.py new file mode 100644 index 0000000..88d7082 --- /dev/null +++ b/support/test/python/multiple_files/cow.py @@ -0,0 +1,15 @@ +def Say( *args, **kwargs ): + print( *args, **kwargs ) + + + + + + + + + + + +def Quiet(): + pass diff --git a/support/test/python/multiple_files/moo.py b/support/test/python/multiple_files/moo.py new file mode 100644 index 0000000..f8da4b1 --- /dev/null +++ b/support/test/python/multiple_files/moo.py @@ -0,0 +1,13 @@ +import cow + + +def Moo(): + for i in range( 1, 100 ): + cow.Say( 'Moo' ) + + for i in range( 1, 100 ): + cow.Say( 'Ooom' ) + + +if __name__ == '__main__': + Moo() diff --git a/support/test/python/no_conf/main.py b/support/test/python/no_conf/main.py new file mode 100755 index 0000000..6515c3f --- /dev/null +++ b/support/test/python/no_conf/main.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python + + +class TestClass( object ): + def __init__( self, value ): + self._var = value + try: + self.DoSomething() + except ValueError: + pass + + def DoSomething( self ): + for i in range( 0, 100 ): + if i < self._var: + print( '{0} is less than the value'.format( i ) ) + else: + print( '{0} might be more'.format( i ) ) + + raise ValueError( 'Done' ) + + +def Main(): + t = TestClass( 18 ) + + t._var = 99 + t.DoSomething() + + +Main() diff --git a/support/test/python/simple_python/.vimspector.json b/support/test/python/simple_python/.vimspector.json index 600295c..69d37a6 100644 --- a/support/test/python/simple_python/.vimspector.json +++ b/support/test/python/simple_python/.vimspector.json @@ -1,7 +1,59 @@ { + "$schema": "https://puremourning.github.io/vimspector/schema/vimspector.schema.json", + "adapters": { + "run_with_debugpy": { + "command": [ "${workspaceRoot}/run_with_debugpy" ], + "port": 9876, + "env": { + "DEBUG_PORT": "9876" + } + }, + "python-remote-docker": { + "variables": { + "port": "8765" + }, + "port": "${port}", + "launch": { + "remote": { + "container": "${ContainerID}", + "runCommand": [ + "python3", "-m", "debugpy", "--listen", "0.0.0.0:${port}", + "--wait-for-client", + "%CMD%" + ] + }, + "delay": "5000m" + } + }, + "python-remote-ssh": { + "variables": { + "port": "8765" + }, + "port": "${port}", + "host": "${host}", + "launch": { + "remote": { + "host": "${host}", + "account": "${account}", + "runCommand": [ + "python3", "-m", "debugpy", "--listen", "0.0.0.0:${port}", + "--wait-for-client", + "%CMD%" + ] + } + } + } + }, "configurations": { - "run": { - "adapter": "vscode-python", + "Use custom gadget": { + "adapter": "test_custom", + "configuration": { + "request": "launch" + } + }, + // This is a comment. + "run legacy vscode-python": { + "adapter": "vscode-python", /* coment goes here too */ "configuration": { "request": "launch", "type": "python", @@ -9,15 +61,143 @@ "program": "${file}", "stopOnEntry": true, "console": "integratedTerminal" + }, + "breakpoints": { + "exception": { + "raised": "N", + "uncaught": "", + "userUnhandled": "" + } } }, "attach": { - "adapter": "vscode-python", + "adapter": "multi-session", + "configuration": { + "request": "attach" + }, + "breakpoints": { + "exception": { + "raised": "N", + "uncaught": "", + "userUnhandled": "" + } + } + }, + "attach-run": { + "adapter": "run_with_debugpy", + "configuration": { + "request": "attach" + }, + "breakpoints": { + "exception": { + "raised": "N", + "uncaught": "", + "userUnhandled": "" + } + } + }, + "docker-attach": { + "adapter": "python-remote-docker", + "remote-cmdLine": [ "/root/main.py" ], + "remote-request": "launch", "configuration": { "request": "attach", + "pathMappings": [ + { + "localRoot": "${workspaceRoot}", + "remoteRoot": "/root" + } + ] + } + }, + "run": { + "adapter": "debugpy", + "configuration": { + "request": "launch", "type": "python", - "host": "localhost", - "port": "5678" + "cwd": "${workspaceRoot}", + "program": "${file}", + "stopOnEntry": false, + "console": "integratedTerminal" + }, + "breakpoints": { + "exception": { + "raised": "N", + "uncaught": "", + "userUnhandled": "" + } + } + }, + "run - default": { + "adapter": "debugpy", + "variables": { + "MAKE_ENV_OUTPUT": { + "shell": "${workspaceRoot}/make_env.sh" + } + }, + "configuration": { + "request": "launch", + "type": "python", + "cwd": "${workspaceRoot}", + "program": "${program:${file\\}}", + "stopOnEntry#json": "${StopOnEntry:true}", + "console": "integratedTerminal", + "args#json": "${args:[]}", + "igored#json#s": "string not json", + "env#json": "${MAKE_ENV_OUTPUT}" + }, + "breakpoints": { + "exception": { + "raised": "N", + "uncaught": "", + "userUnhandled": "" + } + } + }, + "run - main.py": { + "adapter": "debugpy", + "configuration": { + "request": "launch", + "type": "python", + "cwd": "${workspaceRoot}", + "program": "${workspaceRoot}/main.py", + "stopOnEntry": false, + "console": "integratedTerminal" + }, + "breakpoints": { + "exception": { + "raised": "N", + "uncaught": "", + "userUnhandled": "" + } + } + }, + "run - exception question": { + "adapter": "debugpy", + "configuration": { + "request": "launch", + "type": "python", + "cwd": "${workspaceRoot}", + "program": "${file}", + "stopOnEntry": false, + "console": "integratedTerminal" + } + }, + "run - remote host": { + "adapter": "python-remote-ssh", + "remote-cmdLine": [ + "${remoteRoot}/main.py" + ], + "remote-request": "launch", + "configuration": { + "request": "attach", + "redirectOutput": true, + "pathMappings": [ + { + "localRoot": "${workspaceRoot}", + "remoteRoot": "${remoteRoot}" + } + ] } } } diff --git a/support/test/python/simple_python/Dockerfile b/support/test/python/simple_python/Dockerfile new file mode 100644 index 0000000..7748fef --- /dev/null +++ b/support/test/python/simple_python/Dockerfile @@ -0,0 +1,22 @@ +FROM ubuntu:18.04 + +RUN apt-get update && \ + apt-get -y dist-upgrade && \ + apt-get -y install sudo \ + lsb-release \ + ca-certificates \ + python3-dev \ + python3-pip \ + ca-cacert \ + locales \ + language-pack-en \ + libncurses5-dev libncursesw5-dev \ + git && \ + apt-get -y autoremove + +## cleanup of files from setup +RUN rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* + +RUN pip3 install debugpy + +ADD main.py /root/main.py diff --git a/support/test/python/simple_python/build-container b/support/test/python/simple_python/build-container new file mode 100755 index 0000000..bc6450a --- /dev/null +++ b/support/test/python/simple_python/build-container @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +set -e + +if [ "$1" = "--continue" ]; then + OPTS="" +else + OPTS="--no-cache" +fi + +docker build ${OPTS} -t puremourning/vimspector:simple_python . diff --git a/support/test/python/simple_python/make_env.sh b/support/test/python/simple_python/make_env.sh new file mode 100755 index 0000000..a4da8ca --- /dev/null +++ b/support/test/python/simple_python/make_env.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +cat <<"EOF" +{ + "Something": "Value1", + "SomethingElse": "Value2" +} +EOF + diff --git a/support/test/python/simple_python/print_env.py b/support/test/python/simple_python/print_env.py new file mode 100644 index 0000000..4b88f2d --- /dev/null +++ b/support/test/python/simple_python/print_env.py @@ -0,0 +1,15 @@ +#!/usr/bin/env python + +import os + + +def Main(): + print( os.environ.get( 'Something', 'ERROR' ) ) + print( os.environ.get( 'SomethingElse', 'ERROR' ) ) + + for k, v in os.environ: + print( f'{ k } = "{ v }"' ) + + +Main() + diff --git a/support/test/python/simple_python/requirements.txt b/support/test/python/simple_python/requirements.txt index dffcdab..2802a6b 100644 --- a/support/test/python/simple_python/requirements.txt +++ b/support/test/python/simple_python/requirements.txt @@ -1 +1 @@ -ptvsd==4.3.2 +debugpy diff --git a/support/test/python/simple_python/run_with_debugpy b/support/test/python/simple_python/run_with_debugpy new file mode 100755 index 0000000..950ce83 --- /dev/null +++ b/support/test/python/simple_python/run_with_debugpy @@ -0,0 +1,19 @@ +#!/usr/bin/env bash + +if [ -z "$DEBUG_PORT" ]; then + DEBUG_PORT=5678 +fi + +pushd $(dirname $0) + if [ -d env ]; then + rm -rf env + fi + + python3 -m venv env + . env/bin/activate + python -m pip install -r requirements.txt + echo "using port $DEBUG_PORT" + echo "*** Launching ***" + python -m debugpy --wait-for-client --listen 0.0.0.0:${DEBUG_PORT} main.py +popd + diff --git a/support/test/python/simple_python/run_with_ptvsd b/support/test/python/simple_python/run_with_ptvsd deleted file mode 100755 index 6b8c401..0000000 --- a/support/test/python/simple_python/run_with_ptvsd +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/env bash - -PYTHON=python3 - -if [ -n "$1" ]; then - PYTHON=$1 -fi - -pushd $(dirname $0) - if [ ! -d env ]; then - virtualenv -p $PYTHON env - fi - - . env/bin/activate - pip install -r requirements.txt - - python -m ptvsd --wait --host localhost --port 5678 main.py -popd - diff --git a/support/test/ruby/.bundle/config b/support/test/ruby/.bundle/config new file mode 100644 index 0000000..2369228 --- /dev/null +++ b/support/test/ruby/.bundle/config @@ -0,0 +1,2 @@ +--- +BUNDLE_PATH: "vendor/bundle" diff --git a/support/test/ruby/.gitignore b/support/test/ruby/.gitignore new file mode 100644 index 0000000..fc0674a --- /dev/null +++ b/support/test/ruby/.gitignore @@ -0,0 +1,2 @@ +Gemfile.lock +vendor/ diff --git a/support/test/ruby/.vimspector.json b/support/test/ruby/.vimspector.json new file mode 100644 index 0000000..418eb02 --- /dev/null +++ b/support/test/ruby/.vimspector.json @@ -0,0 +1,15 @@ +{ + "configurations": { + "launch current file": { + "adapter": "cust_vscode-ruby", + "configuration": { + "request": "launch", + "program": "${file}", + "args": [ "*${args}" ], + "useBundler": true, + "trace": true, + "showDebuggerOutput": true + } + } + } +} diff --git a/support/test/ruby/Gemfile b/support/test/ruby/Gemfile new file mode 100644 index 0000000..d4580eb --- /dev/null +++ b/support/test/ruby/Gemfile @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +source "https://rubygems.org" + +git_source(:github) {|repo_name| "https://github.com/#{repo_name}" } + +# gem "rails" + +gem "ruby-debug-ide", "~> 0.7.2" + +gem "debase", "~> 0.2.4" diff --git a/support/test/ruby/test.rb b/support/test/ruby/test.rb new file mode 100644 index 0000000..5209e0f --- /dev/null +++ b/support/test/ruby/test.rb @@ -0,0 +1 @@ +print 'hello' 'world' diff --git a/support/test/rust/vimspector_test/.gitignore b/support/test/rust/vimspector_test/.gitignore new file mode 100644 index 0000000..2f7896d --- /dev/null +++ b/support/test/rust/vimspector_test/.gitignore @@ -0,0 +1 @@ +target/ diff --git a/support/test/rust/vimspector_test/.vimspector.json b/support/test/rust/vimspector_test/.vimspector.json new file mode 100644 index 0000000..6918e47 --- /dev/null +++ b/support/test/rust/vimspector_test/.vimspector.json @@ -0,0 +1,12 @@ +{ + "$schema": "https://puremourning.github.io/vimspector/schema/vimspector.schema.json", + "configurations": { + "Run - CodeLLDB": { + "adapter": "CodeLLDB", + "configuration": { + "request": "launch", + "program": "${workspaceRoot}/target/debug/vimspector_test" + } + } + } +} diff --git a/support/test/rust/vimspector_test/Cargo.lock b/support/test/rust/vimspector_test/Cargo.lock new file mode 100644 index 0000000..504f9c0 --- /dev/null +++ b/support/test/rust/vimspector_test/Cargo.lock @@ -0,0 +1,5 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +[[package]] +name = "vimspector_test" +version = "0.1.0" diff --git a/support/test/rust/vimspector_test/Cargo.toml b/support/test/rust/vimspector_test/Cargo.toml new file mode 100644 index 0000000..2440a01 --- /dev/null +++ b/support/test/rust/vimspector_test/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "vimspector_test" +version = "0.1.0" +authors = ["Ben Jackson "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/support/test/rust/vimspector_test/src/main.rs b/support/test/rust/vimspector_test/src/main.rs new file mode 100644 index 0000000..f6fc72d --- /dev/null +++ b/support/test/rust/vimspector_test/src/main.rs @@ -0,0 +1,4 @@ +fn main() { + let s = "World!"; + println!("Hello, {}!", s); +} diff --git a/support/test/tcl/.tclpro/extensions/test.pdx b/support/test/tcl/.tclpro/extensions/test.pdx new file mode 100644 index 0000000..5cc928c --- /dev/null +++ b/support/test/tcl/.tclpro/extensions/test.pdx @@ -0,0 +1,5 @@ +instrument::addExtension 2.0 {test} + +instrument::addCommand Wrap { parseBody } + +# vim: ft=tcl diff --git a/support/test/tcl/.vimspector.json b/support/test/tcl/.vimspector.json new file mode 100644 index 0000000..6959778 --- /dev/null +++ b/support/test/tcl/.vimspector.json @@ -0,0 +1,11 @@ +{ + "configurations": { + "Run Current Script": { + "adapter": "tclpro", + "configuration": { + "request": "launch", + "target": "${file}" + } + } + } +} diff --git a/support/test/tcl/test b/support/test/tcl/test new file mode 100644 index 0000000..a77f56f --- /dev/null +++ b/support/test/tcl/test @@ -0,0 +1,30 @@ +#!/usr/bin/env tclsh + +set SCALAR g +array set ARRAY {key1 value1 key2 value2} + +set LIST [list a b c {def} {g h i j} k l m] + +proc Wrap { body } { + uplevel 1 $body +} + +proc Main {} { + global SCALAR + set prefix "VAR: " + Wrap { + puts $SCALAR + global ARRAY + puts [array names ARRAY] + + set vars [list] + foreach n [array names ::env] { + set prefix "ENVVAR: $n = " + puts "$prefix $::env($n)" + lappend vars $n + } + } + puts $vars +} + +Main diff --git a/syntax/vimspector-installer.vim b/syntax/vimspector-installer.vim new file mode 100644 index 0000000..4eb6170 --- /dev/null +++ b/syntax/vimspector-installer.vim @@ -0,0 +1,21 @@ +if exists( 'b:current_syntax' ) + finish +endif + +let b:current_syntax = 'vimspector-installer' + +syn match VimspectorGadget /[^ ]*\ze@/ +syn match VimspectorGadgetVersion /@\@<=[^ ]*\ze\.\.\./ + + +syn keyword VimspectorInstalling Installing +syn keyword VimspectorDone Done +syn keyword VimspectorSkip Skip +syn keyword VimspectorError Failed FAILED + +hi default link VimspectorInstalling Constant +hi default link VimspectorDone DiffAdd +hi default link VimspectorSkip DiffAdd +hi default link VimspectorError WarningMsg +hi default link VimspectorGadget String +hi default link VimspectorGadgetVersion Identifier diff --git a/tests/.gitignore b/tests/.gitignore new file mode 100644 index 0000000..333c1e9 --- /dev/null +++ b/tests/.gitignore @@ -0,0 +1 @@ +logs/ diff --git a/tests/.vimspector.json b/tests/.vimspector.json new file mode 100644 index 0000000..2a96d66 --- /dev/null +++ b/tests/.vimspector.json @@ -0,0 +1,21 @@ +{ + "configurations": { + "Run test": { + "adapter": "vim-debug-adapter", + "configuration": { + "request": "launch", + "cwd": "${workspaceRoot}", + "env": { + "VIMSPECTOR_MIMODE": "lldb" + }, + "args": [ + "--clean", + "--not-a-term", + "-S", "lib/run_test.vim", + "${file}", + "${TestFunction}" + ] + } + } + } +} diff --git a/tests/breakpoints.test.vim b/tests/breakpoints.test.vim index 9faf08e..877c1cf 100644 --- a/tests/breakpoints.test.vim +++ b/tests/breakpoints.test.vim @@ -1,43 +1,12 @@ function! SetUp() call vimspector#test#setup#SetUpWithMappings( v:none ) + call ThisTestIsFlaky() endfunction function! ClearDown() call vimspector#test#setup#ClearDown() endfunction -function! SetUp_Test_Mappings_Are_Added_HUMAN() - let g:vimspector_enable_mappings = 'HUMAN' -endfunction - -function! Test_Mappings_Are_Added_HUMAN() - call assert_true( hasmapto( 'vimspector#Continue()' ) ) - call assert_false( hasmapto( 'vimspector#Launch()' ) ) - call assert_true( hasmapto( 'vimspector#Stop()' ) ) - call assert_true( hasmapto( 'vimspector#Restart()' ) ) - call assert_true( hasmapto( 'vimspector#ToggleBreakpoint()' ) ) - call assert_true( hasmapto( 'vimspector#AddFunctionBreakpoint' ) ) - call assert_true( hasmapto( 'vimspector#StepOver()' ) ) - call assert_true( hasmapto( 'vimspector#StepInto()' ) ) - call assert_true( hasmapto( 'vimspector#StepOut()' ) ) -endfunction - -function! SetUp_Test_Mappings_Are_Added_VISUAL_STUDIO() - let g:vimspector_enable_mappings = 'VISUAL_STUDIO' -endfunction - -function! Test_Mappings_Are_Added_VISUAL_STUDIO() - call assert_true( hasmapto( 'vimspector#Continue()' ) ) - call assert_false( hasmapto( 'vimspector#Launch()' ) ) - call assert_true( hasmapto( 'vimspector#Stop()' ) ) - call assert_true( hasmapto( 'vimspector#Restart()' ) ) - call assert_true( hasmapto( 'vimspector#ToggleBreakpoint()' ) ) - call assert_true( hasmapto( 'vimspector#AddFunctionBreakpoint' ) ) - call assert_true( hasmapto( 'vimspector#StepOver()' ) ) - call assert_true( hasmapto( 'vimspector#StepInto()' ) ) - call assert_true( hasmapto( 'vimspector#StepOut()' ) ) -endfunction - function! SetUp_Test_Signs_Placed_Using_API_Are_Shown() let g:vimspector_enable_mappings = 'VISUAL_STUDIO' endfunction @@ -53,14 +22,16 @@ function! Test_Signs_Placed_Using_API_Are_Shown() call assert_true( exists( '*vimspector#ToggleBreakpoint' ) ) call vimspector#test#signs#AssertSignGroupSingletonAtLine( 'VimspectorBP', \ line( '.' ), - \ 'vimspectorBP' ) + \ 'vimspectorBP', + \ 9 ) " Disable breakpoint call vimspector#ToggleBreakpoint() call vimspector#test#signs#AssertSignGroupSingletonAtLine( \ 'VimspectorBP', \ line( '.' ), - \ 'vimspectorBPDisabled' ) + \ 'vimspectorBPDisabled', + \ 9 ) " Remove breakpoint call vimspector#ToggleBreakpoint() @@ -72,65 +43,7 @@ function! Test_Signs_Placed_Using_API_Are_Shown() call vimspector#test#signs#AssertSignGroupEmpty( 'VimspectorBP' ) call vimspector#test#signs#AssertSignGroupEmpty( 'VimspectorCode' ) - %bwipeout! -endfunction - -function! SetUp_Test_Use_Mappings_HUMAN() - let g:vimspector_enable_mappings = 'HUMAN' -endfunction - -function! Test_Use_Mappings_HUMAN() - lcd testdata/cpp/simple - edit simple.cpp - call setpos( '.', [ 0, 15, 1 ] ) - - call vimspector#test#signs#AssertCursorIsAtLineInBuffer( 'simple.cpp', 15, 1 ) - call vimspector#test#signs#AssertSignGroupEmptyAtLine( 'VimspectorBP', 15 ) - - " Add the breakpoint - call feedkeys( "\", 'xt' ) - call vimspector#test#signs#AssertSignGroupSingletonAtLine( 'VimspectorBP', - \ 15, - \ 'vimspectorBP' ) - - " Disable the breakpoint - call feedkeys( "\", 'xt' ) - call vimspector#test#signs#AssertSignGroupSingletonAtLine( - \ 'VimspectorBP', - \ 15, - \ 'vimspectorBPDisabled' ) - - " Delete the breakpoint - call feedkeys( "\", 'xt' ) - call vimspector#test#signs#AssertSignGroupEmptyAtLine( 'VimspectorBP', 15 ) - - " Add it again - call feedkeys( "\", 'xt' ) - call vimspector#test#signs#AssertSignGroupSingletonAtLine( - \ 'VimspectorBP', - \ 15, - \ 'vimspectorBP' ) - - " Here we go. Start Debugging - call feedkeys( "\", 'xt' ) - - call assert_equal( 2, len( gettabinfo() ) ) - let cur_tabnr = tabpagenr() - call assert_equal( 5, len( gettabinfo( cur_tabnr )[ 0 ].windows ) ) - - call vimspector#test#signs#AssertCursorIsAtLineInBuffer( 'simple.cpp', 15, 1 ) - - " Step - call feedkeys( "\", 'xt' ) - - call vimspector#test#signs#AssertCursorIsAtLineInBuffer( 'simple.cpp', 16, 1 ) - call WaitForAssert( {-> - \ vimspector#test#signs#AssertPCIsAtLineInBuffer( 'simple.cp', 16 ) - \ } ) - call vimspector#test#setup#Reset() - - lcd - %bwipeout! endfunction @@ -183,7 +96,8 @@ function Test_DisableBreakpointWhileDebugging() \ vimspector#test#signs#AssertSignGroupSingletonAtLine( \ 'VimspectorCode', \ 16, - \ 'vimspectorBP' ) + \ 'vimspectorBP', + \ 9 ) \ } ) " Remove the breakpoint @@ -193,17 +107,17 @@ function Test_DisableBreakpointWhileDebugging() \ 16 ) \ } ) - " Add the breakpoint - call feedkeys( "\", 'xt' ) + call setpos( '.', [ 0, 1, 1 ] ) + call vimspector#SetLineBreakpoint( 'simple.cpp', 16 ) call WaitForAssert( {-> \ vimspector#test#signs#AssertSignGroupSingletonAtLine( \ 'VimspectorCode', \ 16, - \ 'vimspectorBP' ) + \ 'vimspectorBP', + \ 9 ) \ } ) " Run to breakpoint - call setpos( '.', [ 0, 15, 1 ] ) call feedkeys( "\", 'xt' ) call vimspector#test#signs#AssertCursorIsAtLineInBuffer( 'simple.cpp', 16, 1 ) call WaitForAssert( {-> @@ -223,7 +137,8 @@ function Test_DisableBreakpointWhileDebugging() call vimspector#test#signs#AssertSignGroupSingletonAtLine( \ 'VimspectorBP', \ 16, - \ 'vimspectorBP' ) + \ 'vimspectorBP', + \ 9 ) " Disable the breakpoint call setpos( '.', [ bufnr( 'simple.cpp' ), 16, 1 ] ) @@ -231,7 +146,8 @@ function Test_DisableBreakpointWhileDebugging() call vimspector#test#signs#AssertSignGroupSingletonAtLine( \ 'VimspectorBP', \ 16, - \ 'vimspectorBPDisabled' ) + \ 'vimspectorBPDisabled', + \ 9 ) " And delete it call feedkeys( "\", 'xt' ) @@ -244,5 +160,595 @@ function Test_DisableBreakpointWhileDebugging() call vimspector#test#signs#AssertSignGroupEmpty( 'VimspectorCode' ) lcd - + call vimspector#test#setup#Reset() %bwipeout! endfunction + +function! Test_Add_Breakpoints_In_File_Then_Open() + lcd testdata/cpp/simple + + " Set and clear without file open + call vimspector#SetLineBreakpoint( 'simple.cpp', 16 ) + call vimspector#ClearLineBreakpoint( 'simple.cpp', 16 ) + + " Clear non-set breakpoint + call vimspector#ClearLineBreakpoint( 'simple.cpp', 1 ) + + " Re-add + call vimspector#SetLineBreakpoint( 'simple.cpp', 16 ) + + " Open and expect sign to be added + edit simple.cpp + call vimspector#test#signs#AssertSignGroupSingletonAtLine( 'VimspectorBP', + \ 16, + \ 'vimspectorBP', + \ 9 ) + call vimspector#LaunchWithSettings( { 'configuration': 'run-to-breakpoint' } ) + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( 'simple.cpp', 16, 1 ) + + call vimspector#test#setup#Reset() + lcd - + %bwipeout! +endfunction + +function! Test_Add_Breakpoints_In_NonOpenedFile_RunToBreak() + lcd testdata/cpp/simple + + " add + call vimspector#SetLineBreakpoint( 'simple.cpp', 16 ) + + call vimspector#LaunchWithSettings( { + \ 'configuration': 'run-to-breakpoint-specify-file', + \ 'prog': 'simple' + \ } ) + call WaitFor( {-> bufexists( 'simple.cpp' ) } ) + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( 'simple.cpp', 16, 1 ) + call vimspector#test#signs#AssertPCIsAtLineInBuffer( 'simple.cpp', 16 ) + + call vimspector#test#signs#AssertSignAtLine( + \ 'VimspectorCode', + \ 16, + \ 'vimspectorPCBP', + \ 200 ) + + call vimspector#test#setup#Reset() + lcd - + %bwipeout! +endfunction + +function! SetUp_Test_Insert_Code_Above_Breakpoint() + let g:vimspector_enable_mappings = 'HUMAN' +endfunction + +function! Test_Insert_Code_Above_Breakpoint() + let fn='main.py' + lcd ../support/test/python/simple_python + exe 'edit ' . fn + call setpos( '.', [ 0, 25, 5 ] ) + + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( fn, 25, 5 ) + call vimspector#test#signs#AssertSignGroupEmptyAtLine( 'VimspectorBP', 25 ) + + " Add the breakpoint + call feedkeys( "\", 'xt' ) + call vimspector#test#signs#AssertSignGroupSingletonAtLine( 'VimspectorBP', + \ 25, + \ 'vimspectorBP', + \ 9 ) + + " Insert a line above the breakpoint + call append( 22, ' # Test' ) + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( fn, 26, 5 ) + call vimspector#test#signs#AssertSignGroupSingletonAtLine( 'VimspectorBP', + \ 26, + \ 'vimspectorBP', + \ 9 ) + + " CHeck that we break at the right point + call setpos( '.', [ 0, 1, 1 ] ) + call vimspector#LaunchWithSettings( { 'configuration': 'run' } ) + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( fn, 26, 1 ) + call vimspector#Reset() + call vimspector#test#setup#WaitForReset() + + " Toggle the breakpoint + call setpos( '.', [ 0, 26, 1 ] ) + call vimspector#test#signs#AssertSignGroupSingletonAtLine( 'VimspectorBP', + \ 26, + \ 'vimspectorBP', + \ 9 ) + call feedkeys( "\", 'xt' ) + call vimspector#test#signs#AssertSignGroupSingletonAtLine( + \ 'VimspectorBP', + \ 26, + \ 'vimspectorBPDisabled', + \ 9 ) + " Delete it + call feedkeys( "\", 'xt' ) + call vimspector#test#signs#AssertSignGroupEmptyAtLine( 'VimspectorBP', 26 ) +endfunction + +function! SetUp_Test_Conditional_Line_Breakpoint() + let g:vimspector_enable_mappings = 'HUMAN' +endfunction + +function! Test_Conditional_Line_Breakpoint() + lcd testdata/cpp/simple + edit simple.cpp + call setpos( '.', [ 0, 16, 1 ] ) + + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( 'simple.cpp', 16, 1 ) + call vimspector#test#signs#AssertSignGroupEmptyAtLine( 'VimspectorBP', 16 ) + + " Add the conditional breakpoint (note , is the mapleader) + call feedkeys( ",\argc==0\\", 'xt' ) + call vimspector#test#signs#AssertSignGroupSingletonAtLine( 'VimspectorBP', + \ 16, + \ 'vimspectorBPCond', + \ 9 ) + + " Disable the breakpoint + call feedkeys( "\", 'xt' ) + call vimspector#test#signs#AssertSignGroupSingletonAtLine( + \ 'VimspectorBP', + \ 16, + \ 'vimspectorBPDisabled', + \ 9 ) + + " Delete the breakpoint + call feedkeys( "\", 'xt' ) + call vimspector#test#signs#AssertSignGroupEmptyAtLine( 'VimspectorBP', 16 ) + + " Add breakpoint using API: + " - on line 16 condition which doesn't match + " - then an unconditional one on line 9, unconditional + " - then on line 17, condition which matches + call vimspector#ToggleBreakpoint( { 'condition': 'argc == 0' } ) + call vimspector#test#signs#AssertSignGroupSingletonAtLine( + \ 'VimspectorBP', + \ 16, + \ 'vimspectorBPCond', + \ 9 ) + call setpos( '.', [ 0, 9, 1 ] ) + call vimspector#ToggleBreakpoint() + call vimspector#test#signs#AssertSignGroupSingletonAtLine( + \ 'VimspectorBP', + \ 9, + \ 'vimspectorBP', + \ 9 ) + + call setpos( '.', [ 0, 1, 1 ] ) + call vimspector#SetLineBreakpoint( + \ 'simple.cpp', + \ 17, + \ { 'condition': 'argc == 1' } ) + call vimspector#test#signs#AssertSignGroupSingletonAtLine( + \ 'VimspectorBP', + \ 17, + \ 'vimspectorBPCond', + \ 9 ) + + call setpos( '.', [ 0, 1, 1 ] ) + + " Start debugging + call vimspector#Continue() + " break on main + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( 'simple.cpp', 15, 1 ) + + " Ignore non-matching on line 16, break on line 9 + call vimspector#Continue() + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( 'simple.cpp', 9, 1 ) + + " Condition matches on line 17 + call vimspector#Continue() + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( 'simple.cpp', 17, 1 ) + + call vimspector#test#setup#Reset() + + lcd - + %bwipeout! +endfunction + +function! SetUp_Test_Conditional_Line_Breakpoint_Hit() + let g:vimspector_enable_mappings = 'HUMAN' +endfunction + +function! Test_Conditional_Line_Breakpoint_Hit() + call ThisTestIsFlaky() + + let fn = '../support/test/python/simple_python/main.py' + exe 'edit' fn + call setpos( '.', [ 0, 14, 1 ] ) + + " Add the conditional breakpoint (3 times) (note , is the mapleader) + call feedkeys( ",\\3\", 'xt' ) + call vimspector#test#signs#AssertSignGroupSingletonAtLine( + \ 'VimspectorBP', + \ 14, + \ 'vimspectorBPCond', + \ 9 ) + + call vimspector#LaunchWithSettings( { 'configuration': 'run' } ) + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( fn, 14, 1 ) + + " difficult to check if we really did run 3 times, so just use the watch + " window (also, tests the watch window!) + call vimspector#AddWatch( 'i' ) + call WaitForAssert( {-> + \ assert_equal( [ ' *- Result: 2' ], + \ getbufline( 'vimspector.Watches', '$' ) ) + \ } ) + + + call vimspector#test#setup#Reset() + %bwipeout! +endfunction + +function! Test_Function_Breakpoint() + lcd testdata/cpp/simple + edit simple.cpp + call vimspector#AddFunctionBreakpoint( 'foo' ) + call vimspector#Launch() + " break on main + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( 'simple.cpp', 15, 1 ) + call vimspector#Continue() + " break on func + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( 'simple.cpp', 9, 1 ) + call vimspector#test#setup#Reset() + %bwipeout! +endfunction + +function! Test_Function_Breakpoint_Condition() + lcd testdata/cpp/simple + edit simple.cpp + call vimspector#AddFunctionBreakpoint( 'foo', { 'condition': '1' } ) + call vimspector#Launch() + " break on main + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( 'simple.cpp', 15, 1 ) + call vimspector#Continue() + " break on func + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( 'simple.cpp', 9, 1 ) + call vimspector#test#setup#Reset() + %bwipeout! +endfunction + +" Can't find an adapter that supports conditional function breakpoints which are +" probably pretty niche anyway +" +" function! Test_Function_Breakpoint_Condition_False() +" lcd testdata/cpp/simple +" edit simple.cpp +" +" call vimspector#AddFunctionBreakpoint( 'foo', { 'condition': '0' } ) +" call setpos( '.', [ 0, 17, 1 ] ) +" call vimspector#ToggleBreakpoint() +" call vimspector#Launch() +" " break on main +" call vimspector#test#signs#AssertCursorIsAtLineInBuffer( 'simple.cpp', 15, 1 ) +" call vimspector#Continue() +" +" " doesn't break in func, break on line 17 +" call vimspector#test#signs#AssertCursorIsAtLineInBuffer( 'simple.cpp', 17, 1 ) +" call vimspector#test#setup#Reset() +" %bwipeout! +" throw "xfail cpptools doesn't seem to honour conditions on function bps" +" endfunction + +function! s:CheckQuickFixEntries( entries ) + let qf = getqflist() + let i = 0 + for entry in a:entries + if i >= len( qf ) + call assert_report( 'Expected more quickfix entries' ) + endif + for key in keys( entry ) + call assert_equal( entry[ key ], + \ qf[ i ][ key ], + \ key . ' in ' . string( qf[ i ] ) + \ . ' expected ' . entry[ key ] ) + endfor + let i = i+1 + endfor +endfunction + +function! Test_ListBreakpoints() + lcd testdata/cpp/simple + edit simple.cpp + call setpos( '.', [ 0, 15, 1 ] ) + + call vimspector#ListBreakpoints() + wincmd p + cclose + call s:CheckQuickFixEntries( [] ) + + call vimspector#ToggleBreakpoint() + call assert_equal( [], getqflist() ) + + call vimspector#ListBreakpoints() + call s:CheckQuickFixEntries( [ + \ { 'lnum': 15, 'col': 1, 'bufnr': bufnr( 'simple.cpp', 0 ) } + \ ] ) + + " Cursor jumps to the quickfix window + call assert_equal( 'quickfix', &buftype ) + cclose + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( 'simple.cpp', 15, 1 ) + + call vimspector#Launch() + " break on main + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( 'simple.cpp', 15, 1 ) + + call vimspector#ListBreakpoints() + call s:CheckQuickFixEntries( [ + \ { 'lnum': 15, 'col': 1, 'bufnr': bufnr( 'simple.cpp', 0 ) } + \ ] ) + call assert_equal( 'quickfix', &buftype ) + wincmd p + cclose + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( 'simple.cpp', 15, 1 ) + + " Add a breakpoint that moves (from line 5 to line 9) + call cursor( [ 5, 1 ] ) + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( 'simple.cpp', 5, 1 ) + call vimspector#ToggleBreakpoint() + + function! Check() + call vimspector#ListBreakpoints() + wincmd p + return assert_equal( 2, len( getqflist() ) ) + endfunction + call WaitForAssert( function( 'Check' ) ) + + call s:CheckQuickFixEntries( [ + \ { 'lnum': 15, 'col': 1, 'bufnr': bufnr( 'simple.cpp', 0 ) }, + \ { 'lnum': 9, 'col': 1, 'bufnr': bufnr( 'simple.cpp', 0 ) }, + \ ] ) + + call vimspector#test#setup#Reset() + %bwipe! +endfunction + +function! Test_Custom_Breakpoint_Priority() + let g:vimspector_sign_priority = { + \ 'vimspectorPC': 1, + \ 'vimspectorPCBP': 1, + \ 'vimspectorBP': 2, + \ 'vimspectorBPCond': 3, + \ 'vimspectorBPDisabled': 4 + \ } + + " While not debugging + lcd testdata/cpp/simple + edit simple.cpp + + call setpos( '.', [ 0, 15, 1 ] ) + call vimspector#ToggleBreakpoint() + call vimspector#test#signs#AssertSignGroupSingletonAtLine( 'VimspectorBP', + \ 15, + \ 'vimspectorBP', + \ 2 ) + call setpos( '.', [ 0, 16, 1 ] ) + call vimspector#ToggleBreakpoint() + call vimspector#ToggleBreakpoint() + call vimspector#test#signs#AssertSignGroupSingletonAtLine( + \ 'VimspectorBP', + \ 16, + \ 'vimspectorBPDisabled', + \ 4 ) + call vimspector#ToggleBreakpoint() + call vimspector#test#signs#AssertSignGroupEmptyAtLine( 'VimspectorBP', 16 ) + + call setpos( '.', [ 0, 17, 1 ] ) + call vimspector#ToggleBreakpoint( { 'condition': '1' } ) + call vimspector#test#signs#AssertSignGroupSingletonAtLine( + \ 'VimspectorBP', + \ 17, + \ 'vimspectorBPCond', + \ 3 ) + + " While debugging + call vimspector#Launch() + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( 'simple.cpp', 15, 1 ) + call vimspector#test#signs#AssertPCIsAtLineInBuffer( 'simple.cpp', 15 ) + call vimspector#test#signs#AssertSignAtLine( + \ 'VimspectorCode', + \ 15, + \ 'vimspectorBP', + \ 2 ) + call vimspector#test#signs#AssertSignAtLine( + \ 'VimspectorCode', + \ 15, + \ 'vimspectorPCBP', + \ 1 ) + call vimspector#test#signs#AssertSignGroupSingletonAtLine( 'VimspectorCode', + \ 17, + \ 'vimspectorBP', + \ 2 ) + + call vimspector#StepOver() + " No sign as disabled + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( 'simple.cpp', 16, 1 ) + call vimspector#test#signs#AssertPCIsAtLineInBuffer( 'simple.cpp', 16 ) + + call vimspector#StepOver() + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( 'simple.cpp', 17, 1 ) + call vimspector#test#signs#AssertPCIsAtLineInBuffer( 'simple.cpp', 17 ) + + call vimspector#test#signs#AssertSignGroupSingletonAtLine( + \ 'VimspectorCode', + \ 15, + \ 'vimspectorBP', + \ 2 ) + call vimspector#test#signs#AssertSignAtLine( + \ 'VimspectorCode', + \ 17, + \ 'vimspectorBP', + \ 2 ) + call vimspector#test#signs#AssertSignAtLine( + \ 'VimspectorCode', + \ 17, + \ 'vimspectorPCBP', + \ 1 ) + + + call vimspector#test#setup#Reset() + lcd - + %bwipeout! + unlet! g:vimspector_sign_priority +endfunction + +function! Test_Custom_Breakpoint_Priority_Partial() + let g:vimspector_sign_priority = { + \ 'vimspectorBP': 2, + \ 'vimspectorBPCond': 3, + \ 'vimspectorBPDisabled': 4 + \ } + + " While not debugging + lcd testdata/cpp/simple + edit simple.cpp + + call setpos( '.', [ 0, 15, 1 ] ) + call vimspector#ToggleBreakpoint() + call vimspector#test#signs#AssertSignGroupSingletonAtLine( 'VimspectorBP', + \ 15, + \ 'vimspectorBP', + \ 2 ) + call setpos( '.', [ 0, 16, 1 ] ) + call vimspector#ToggleBreakpoint() + call vimspector#ToggleBreakpoint() + call vimspector#test#signs#AssertSignGroupSingletonAtLine( + \ 'VimspectorBP', + \ 16, + \ 'vimspectorBPDisabled', + \ 4 ) + call vimspector#ToggleBreakpoint() + call vimspector#test#signs#AssertSignGroupEmptyAtLine( 'VimspectorBP', 16 ) + + call setpos( '.', [ 0, 17, 1 ] ) + call vimspector#ToggleBreakpoint( { 'condition': '1' } ) + call vimspector#test#signs#AssertSignGroupSingletonAtLine( + \ 'VimspectorBP', + \ 17, + \ 'vimspectorBPCond', + \ 3 ) + + " While debugging + call vimspector#Launch() + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( 'simple.cpp', 15, 1 ) + call vimspector#test#signs#AssertPCIsAtLineInBuffer( 'simple.cpp', 15 ) + call vimspector#test#signs#AssertSignAtLine( + \ 'VimspectorCode', + \ 15, + \ 'vimspectorBP', + \ 2 ) + call vimspector#test#signs#AssertSignAtLine( + \ 'VimspectorCode', + \ 15, + \ 'vimspectorPCBP', + \ 200 ) + call vimspector#test#signs#AssertSignGroupSingletonAtLine( 'VimspectorCode', + \ 17, + \ 'vimspectorBP', + \ 2 ) + + call vimspector#StepOver() + " No sign as disabled + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( 'simple.cpp', 16, 1 ) + call vimspector#test#signs#AssertPCIsAtLineInBuffer( 'simple.cpp', 16 ) + + call vimspector#StepOver() + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( 'simple.cpp', 17, 1 ) + call vimspector#test#signs#AssertPCIsAtLineInBuffer( 'simple.cpp', 17 ) + + call vimspector#test#signs#AssertSignGroupSingletonAtLine( + \ 'VimspectorCode', + \ 15, + \ 'vimspectorBP', + \ 2 ) + call vimspector#test#signs#AssertSignAtLine( + \ 'VimspectorCode', + \ 17, + \ 'vimspectorBP', + \ 2 ) + call vimspector#test#signs#AssertSignAtLine( + \ 'VimspectorCode', + \ 17, + \ 'vimspectorPCBP', + \ 200 ) + + + call vimspector#test#setup#Reset() + lcd - + %bwipeout! + unlet! g:vimspector_sign_priority +endfunction + +function! Test_Add_Line_BP_In_Other_File_While_Debugging() + call ThisTestIsFlaky() + let moo = 'moo.py' + let cow = 'cow.py' + lcd ../support/test/python/multiple_files + exe 'edit' moo + + call vimspector#Launch() + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( moo, 1, 1 ) + call vimspector#test#signs#AssertPCIsAtLineInBuffer( moo, 1 ) + + call cursor( 6, 3 ) + call vimspector#ToggleBreakpoint() + call vimspector#test#signs#AssertPCIsAtLineInBuffer( moo, 1 ) + call WaitForAssert( {-> + \vimspector#test#signs#AssertSignGroupSingletonAtLine( + \ 'VimspectorCode', + \ 6, + \ 'vimspectorBP', + \ 9 ) } ) + + exe 'edit' cow + call cursor( 2, 1 ) + call vimspector#ToggleBreakpoint() + call vimspector#test#signs#AssertSignGroupEmptyAtLine( 'VimspectorCode', 6 ) + call WaitForAssert( {-> + \ vimspector#test#signs#AssertSignGroupSingletonAtLine( + \ 'VimspectorCode', + \ 2, + \ 'vimspectorBP', + \ 9 ) } ) + + call vimspector#Continue() + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( moo, 6, 1 ) + call vimspector#test#signs#AssertPCIsAtLineInBuffer( moo, 6 ) + + call vimspector#test#signs#AssertSignGroupEmptyAtLine( 'VimspectorCode', 2 ) + call vimspector#test#signs#AssertSignAtLine( + \ 'VimspectorCode', + \ 6, + \ 'vimspectorBP', + \ 9 ) + call vimspector#test#signs#AssertSignAtLine( + \ 'VimspectorCode', + \ 6, + \ 'vimspectorPCBP', + \ 200 ) + + call vimspector#Continue() + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( cow, 2, 1 ) + call vimspector#test#signs#AssertPCIsAtLineInBuffer( cow, 2 ) + + call vimspector#test#signs#AssertSignGroupEmptyAtLine( 'VimspectorCode', 6 ) + call vimspector#test#signs#AssertSignAtLine( + \ 'VimspectorCode', + \ 2, + \ 'vimspectorBP', + \ 9 ) + call vimspector#test#signs#AssertSignAtLine( + \ 'VimspectorCode', + \ 2, + \ 'vimspectorPCBP', + \ 200 ) + + lcd - + call vimspector#test#setup#Reset() + %bwipe! +endfunction diff --git a/tests/breakpoints_doublewidth.test.vim b/tests/breakpoints_doublewidth.test.vim new file mode 100644 index 0000000..4bc0571 --- /dev/null +++ b/tests/breakpoints_doublewidth.test.vim @@ -0,0 +1,765 @@ +function! SetUp() + set ambiwidth=double + call vimspector#test#setup#SetUpWithMappings( v:none ) + call ThisTestIsFlaky() +endfunction + +function! ClearDown() + call vimspector#test#setup#ClearDown() +endfunction + +function! SetUp_Test_Signs_Placed_Using_API_Are_Shown() + let g:vimspector_enable_mappings = 'VISUAL_STUDIO' +endfunction + +function! Test_Signs_Placed_Using_API_Are_Shown() + " We need a real file + edit testdata/cpp/simple/simple.cpp + call feedkeys( "/printf\", 'xt' ) + + " Set breakpoint + call vimspector#ToggleBreakpoint() + + call assert_true( exists( '*vimspector#ToggleBreakpoint' ) ) + call vimspector#test#signs#AssertSignGroupSingletonAtLine( 'VimspectorBP', + \ line( '.' ), + \ 'vimspectorBP', + \ 9 ) + + " Disable breakpoint + call vimspector#ToggleBreakpoint() + call vimspector#test#signs#AssertSignGroupSingletonAtLine( + \ 'VimspectorBP', + \ line( '.' ), + \ 'vimspectorBPDisabled', + \ 9 ) + + " Remove breakpoint + call vimspector#ToggleBreakpoint() + + call vimspector#test#signs#AssertSignGroupEmptyAtLine( 'VimspectorBP', + \ line( '.' ) ) + + call vimspector#ClearBreakpoints() + call vimspector#test#signs#AssertSignGroupEmpty( 'VimspectorBP' ) + call vimspector#test#signs#AssertSignGroupEmpty( 'VimspectorCode' ) + + call vimspector#test#setup#Reset() + %bwipeout! +endfunction + +function! SetUp_Test_Use_Mappings_HUMAN() + let g:vimspector_enable_mappings = 'HUMAN' +endfunction + +function! Test_Use_Mappings_HUMAN() + lcd testdata/cpp/simple + edit simple.cpp + call setpos( '.', [ 0, 15, 1 ] ) + + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( 'simple.cpp', 15, 1 ) + call vimspector#test#signs#AssertSignGroupEmptyAtLine( 'VimspectorBP', 15 ) + + " Add the breakpoint + call feedkeys( "\", 'xt' ) + call vimspector#test#signs#AssertSignGroupSingletonAtLine( 'VimspectorBP', + \ 15, + \ 'vimspectorBP', + \ 9 ) + + " Disable the breakpoint + call feedkeys( "\", 'xt' ) + call vimspector#test#signs#AssertSignGroupSingletonAtLine( + \ 'VimspectorBP', + \ 15, + \ 'vimspectorBPDisabled', + \ 9 ) + + " Delete the breakpoint + call feedkeys( "\", 'xt' ) + call vimspector#test#signs#AssertSignGroupEmptyAtLine( 'VimspectorBP', 15 ) + + " Add it again + call feedkeys( "\", 'xt' ) + call vimspector#test#signs#AssertSignGroupSingletonAtLine( + \ 'VimspectorBP', + \ 15, + \ 'vimspectorBP', + \ 9 ) + + " Here we go. Start Debugging + call feedkeys( "\", 'xt' ) + + call assert_equal( 2, len( gettabinfo() ) ) + let cur_tabnr = tabpagenr() + call assert_equal( 5, len( gettabinfo( cur_tabnr )[ 0 ].windows ) ) + + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( 'simple.cpp', 15, 1 ) + + " Step + call feedkeys( "\", 'xt' ) + + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( 'simple.cpp', 16, 1 ) + call WaitForAssert( {-> + \ vimspector#test#signs#AssertPCIsAtLineInBuffer( 'simple.cpp', 16 ) + \ } ) + + call vimspector#test#setup#Reset() + + lcd - + %bwipeout! +endfunction + +function! SetUp_Test_StopAtEntry() + let g:vimspector_enable_mappings = 'HUMAN' +endfunction + +function Test_StopAtEntry() + lcd testdata/cpp/simple + edit simple.cpp + call setpos( '.', [ 0, 1, 1 ] ) + + " Test stopAtEntry behaviour + call feedkeys( "\", 'xt' ) + + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( 'simple.cpp', 15, 1 ) + call WaitForAssert( {-> + \ vimspector#test#signs#AssertPCIsAtLineInBuffer( 'simple.cpp', 15 ) + \ } ) + + call vimspector#test#setup#Reset() + + lcd - + %bwipeout! +endfunction + +function! SetUp_Test_DisableBreakpointWhileDebugging() + let g:vimspector_enable_mappings = 'HUMAN' +endfunction + +function Test_DisableBreakpointWhileDebugging() + lcd testdata/cpp/simple + edit simple.cpp + call setpos( '.', [ 0, 15, 1 ] ) + + " Test stopAtEntry behaviour + call feedkeys( "\", 'xt' ) + + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( 'simple.cpp', 15, 1 ) + call WaitForAssert( {-> + \ vimspector#test#signs#AssertPCIsAtLineInBuffer( 'simple.cpp', 15 ) + \ } ) + call vimspector#test#signs#AssertSignGroupEmpty( 'VimspectorBP' ) + + call setpos( '.', [ 0, 16, 1 ] ) + + " Add the breakpoint + call feedkeys( "\", 'xt' ) + call WaitForAssert( {-> + \ vimspector#test#signs#AssertSignGroupSingletonAtLine( + \ 'VimspectorCode', + \ 16, + \ 'vimspectorBP', + \ 9 ) + \ } ) + + " Remove the breakpoint + call feedkeys( "\", 'xt' ) + call WaitForAssert( {-> + \ vimspector#test#signs#AssertSignGroupEmptyAtLine( 'VimspectorCode', + \ 16 ) + \ } ) + + " Add the breakpoint + call feedkeys( "\", 'xt' ) + call WaitForAssert( {-> + \ vimspector#test#signs#AssertSignGroupSingletonAtLine( + \ 'VimspectorCode', + \ 16, + \ 'vimspectorBP', + \ 9 ) + \ } ) + + " Run to breakpoint + call setpos( '.', [ 0, 15, 1 ] ) + call feedkeys( "\", 'xt' ) + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( 'simple.cpp', 16, 1 ) + call WaitForAssert( {-> + \ vimspector#test#signs#AssertPCIsAtLineInBuffer( 'simple.cpp', 16 ) + \ } ) + + call vimspector#Reset() + call WaitForAssert( {-> + \ assert_true ( pyxeval( '_vimspector_session._connection is None' ) ) + \ } ) + call WaitForAssert( {-> + \ assert_true( pyxeval( '_vimspector_session._uiTab is None' ) ) + \ } ) + + " Check breakpoint is now a user breakpoint + call setpos( '.', [ bufnr( 'simple.cpp' ), 1, 1 ] ) + call vimspector#test#signs#AssertSignGroupSingletonAtLine( + \ 'VimspectorBP', + \ 16, + \ 'vimspectorBP', + \ 9 ) + + " Disable the breakpoint + call setpos( '.', [ bufnr( 'simple.cpp' ), 16, 1 ] ) + call feedkeys( "\", 'xt' ) + call vimspector#test#signs#AssertSignGroupSingletonAtLine( + \ 'VimspectorBP', + \ 16, + \ 'vimspectorBPDisabled', + \ 9 ) + + " And delete it + call feedkeys( "\", 'xt' ) + call vimspector#test#signs#AssertSignGroupEmptyAtLine( + \ 'VimspectorBP', + \ 16 ) + + call vimspector#ClearBreakpoints() + call vimspector#test#signs#AssertSignGroupEmpty( 'VimspectorBP' ) + call vimspector#test#signs#AssertSignGroupEmpty( 'VimspectorCode' ) + + lcd - + call vimspector#test#setup#Reset() + %bwipeout! +endfunction + +function! SetUp_Test_Insert_Code_Above_Breakpoint() + let g:vimspector_enable_mappings = 'HUMAN' +endfunction + +function! Test_Insert_Code_Above_Breakpoint() + let fn='main.py' + lcd ../support/test/python/simple_python + exe 'edit ' . fn + call setpos( '.', [ 0, 25, 5 ] ) + + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( fn, 25, 5 ) + call vimspector#test#signs#AssertSignGroupEmptyAtLine( 'VimspectorBP', 25 ) + + " Add the breakpoint + call feedkeys( "\", 'xt' ) + call vimspector#test#signs#AssertSignGroupSingletonAtLine( 'VimspectorBP', + \ 25, + \ 'vimspectorBP', + \ 9 ) + + " Insert a line above the breakpoint + call append( 22, ' # Test' ) + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( fn, 26, 5 ) + call vimspector#test#signs#AssertSignGroupSingletonAtLine( 'VimspectorBP', + \ 26, + \ 'vimspectorBP', + \ 9 ) + + " CHeck that we break at the right point + call setpos( '.', [ 0, 1, 1 ] ) + call vimspector#LaunchWithSettings( { 'configuration': 'run' } ) + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( fn, 26, 1 ) + call vimspector#Reset() + call vimspector#test#setup#WaitForReset() + + " Toggle the breakpoint + call setpos( '.', [ 0, 26, 1 ] ) + call vimspector#test#signs#AssertSignGroupSingletonAtLine( 'VimspectorBP', + \ 26, + \ 'vimspectorBP', + \ 9 ) + call feedkeys( "\", 'xt' ) + call vimspector#test#signs#AssertSignGroupSingletonAtLine( + \ 'VimspectorBP', + \ 26, + \ 'vimspectorBPDisabled', + \ 9 ) + " Delete it + call feedkeys( "\", 'xt' ) + call vimspector#test#signs#AssertSignGroupEmptyAtLine( 'VimspectorBP', 26 ) + +endfunction + +function! SetUp_Test_Conditional_Line_Breakpoint() + let g:vimspector_enable_mappings = 'HUMAN' +endfunction + +function! Test_Conditional_Line_Breakpoint() + lcd testdata/cpp/simple + edit simple.cpp + call setpos( '.', [ 0, 16, 1 ] ) + + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( 'simple.cpp', 16, 1 ) + call vimspector#test#signs#AssertSignGroupEmptyAtLine( 'VimspectorBP', 16 ) + + " Add the conditional breakpoint (, is mapleader) + call feedkeys( ",\argc==0\\", 'xt' ) + call vimspector#test#signs#AssertSignGroupSingletonAtLine( 'VimspectorBP', + \ 16, + \ 'vimspectorBPCond', + \ 9 ) + + " Disable the breakpoint + call feedkeys( "\", 'xt' ) + call vimspector#test#signs#AssertSignGroupSingletonAtLine( + \ 'VimspectorBP', + \ 16, + \ 'vimspectorBPDisabled', + \ 9 ) + + " Delete the breakpoint + call feedkeys( "\", 'xt' ) + call vimspector#test#signs#AssertSignGroupEmptyAtLine( 'VimspectorBP', 16 ) + + " Add breakpoint using API: + " - on line 16 condition which doesn't match + " - then an unconditional one on line 9, unconditional + " - then on line 17, condition which matches + call vimspector#ToggleBreakpoint( { 'condition': 'argc == 0' } ) + call vimspector#test#signs#AssertSignGroupSingletonAtLine( + \ 'VimspectorBP', + \ 16, + \ 'vimspectorBPCond', + \ 9 ) + call setpos( '.', [ 0, 9, 1 ] ) + call vimspector#ToggleBreakpoint() + call vimspector#test#signs#AssertSignGroupSingletonAtLine( + \ 'VimspectorBP', + \ 9, + \ 'vimspectorBP', + \ 9 ) + + call setpos( '.', [ 0, 17, 1 ] ) + call vimspector#ToggleBreakpoint( { 'condition': 'argc == 1' } ) + call vimspector#test#signs#AssertSignGroupSingletonAtLine( + \ 'VimspectorBP', + \ 17, + \ 'vimspectorBPCond', + \ 9 ) + + call setpos( '.', [ 0, 1, 1 ] ) + + " Start debugging + call vimspector#Continue() + " break on main + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( 'simple.cpp', 15, 1 ) + + " Ignore non-matching on line 16, break on line 9 + call vimspector#Continue() + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( 'simple.cpp', 9, 1 ) + + " Condition matches on line 17 + call vimspector#Continue() + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( 'simple.cpp', 17, 1 ) + + call vimspector#test#setup#Reset() + + lcd - + %bwipeout! +endfunction + +function! SetUp_Test_Conditional_Line_Breakpoint_Hit() + let g:vimspector_enable_mappings = 'HUMAN' +endfunction + +function! Test_Conditional_Line_Breakpoint_Hit() + call ThisTestIsFlaky() + + let fn = '../support/test/python/simple_python/main.py' + exe 'edit' fn + call setpos( '.', [ 0, 14, 1 ] ) + + " Add the conditional breakpoint (3 times) (, is mapleader) + call feedkeys( ",\\3\", 'xt' ) + call vimspector#test#signs#AssertSignGroupSingletonAtLine( + \ 'VimspectorBP', + \ 14, + \ 'vimspectorBPCond', + \ 9 ) + + call vimspector#LaunchWithSettings( { 'configuration': 'run' } ) + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( fn, 14, 1 ) + + " difficult to check if we really did run 3 times, so just use the watch + " window (also, tests the watch window!) + call vimspector#AddWatch( 'i' ) + call WaitForAssert( {-> + \ assert_equal( [ ' *- Result: 2' ], + \ getbufline( 'vimspector.Watches', '$' ) ) + \ } ) + + + call vimspector#test#setup#Reset() + %bwipeout! +endfunction + +function! Test_Function_Breakpoint() + lcd testdata/cpp/simple + edit simple.cpp + call vimspector#AddFunctionBreakpoint( 'foo' ) + call vimspector#Launch() + " break on main + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( 'simple.cpp', 15, 1 ) + call vimspector#Continue() + " break on func + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( 'simple.cpp', 9, 1 ) + call vimspector#test#setup#Reset() + %bwipeout! +endfunction + +function! Test_Function_Breakpoint_Condition() + lcd testdata/cpp/simple + edit simple.cpp + call vimspector#AddFunctionBreakpoint( 'foo', { 'condition': '1' } ) + call vimspector#Launch() + " break on main + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( 'simple.cpp', 15, 1 ) + call vimspector#Continue() + " break on func + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( 'simple.cpp', 9, 1 ) + call vimspector#test#setup#Reset() + %bwipeout! +endfunction + +" Can't find an adapter that supports conditional function breakpoints which are +" probably pretty niche anyway +" +" function! Test_Function_Breakpoint_Condition_False() +" lcd testdata/cpp/simple +" edit simple.cpp +" +" call vimspector#AddFunctionBreakpoint( 'foo', { 'condition': '0' } ) +" call setpos( '.', [ 0, 17, 1 ] ) +" call vimspector#ToggleBreakpoint() +" call vimspector#Launch() +" " break on main +" call vimspector#test#signs#AssertCursorIsAtLineInBuffer( 'simple.cpp', 15, 1 ) +" call vimspector#Continue() +" +" " doesn't break in func, break on line 17 +" call vimspector#test#signs#AssertCursorIsAtLineInBuffer( 'simple.cpp', 17, 1 ) +" call vimspector#test#setup#Reset() +" %bwipeout! +" throw "xfail cpptools doesn't seem to honour conditions on function bps" +" endfunction + +function! s:CheckQuickFixEntries( entries ) + let qf = getqflist() + let i = 0 + for entry in a:entries + if i >= len( qf ) + call assert_report( 'Expected more quickfix entries' ) + endif + for key in keys( entry ) + call assert_equal( entry[ key ], + \ qf[ i ][ key ], + \ key . ' in ' . string( qf[ i ] ) + \ . ' expected ' . entry[ key ] ) + endfor + let i = i+1 + endfor +endfunction + +function! Test_ListBreakpoints() + lcd testdata/cpp/simple + edit simple.cpp + call setpos( '.', [ 0, 15, 1 ] ) + + call vimspector#ListBreakpoints() + wincmd p + cclose + call s:CheckQuickFixEntries( [] ) + + call vimspector#ToggleBreakpoint() + call assert_equal( [], getqflist() ) + + call vimspector#ListBreakpoints() + call s:CheckQuickFixEntries( [ + \ { 'lnum': 15, 'col': 1, 'bufnr': bufnr( 'simple.cpp', 0 ) } + \ ] ) + + " Cursor jumps to the quickfix window + call assert_equal( 'quickfix', &buftype ) + cclose + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( 'simple.cpp', 15, 1 ) + + call vimspector#Launch() + " break on main + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( 'simple.cpp', 15, 1 ) + + call vimspector#ListBreakpoints() + call s:CheckQuickFixEntries( [ + \ { 'lnum': 15, 'col': 1, 'bufnr': bufnr( 'simple.cpp', 0 ) } + \ ] ) + call assert_equal( 'quickfix', &buftype ) + wincmd p + cclose + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( 'simple.cpp', 15, 1 ) + + " Add a breakpoint that moves (from line 5 to line 9) + call cursor( [ 5, 1 ] ) + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( 'simple.cpp', 5, 1 ) + call vimspector#ToggleBreakpoint() + + function! Check() + call vimspector#ListBreakpoints() + wincmd p + return assert_equal( 2, len( getqflist() ) ) + endfunction + call WaitForAssert( function( 'Check' ) ) + + call s:CheckQuickFixEntries( [ + \ { 'lnum': 15, 'col': 1, 'bufnr': bufnr( 'simple.cpp', 0 ) }, + \ { 'lnum': 9, 'col': 1, 'bufnr': bufnr( 'simple.cpp', 0 ) }, + \ ] ) + + call vimspector#test#setup#Reset() + %bwipe! +endfunction + +function! Test_Custom_Breakpoint_Priority() + let g:vimspector_sign_priority = { + \ 'vimspectorPC': 1, + \ 'vimspectorPCBP': 1, + \ 'vimspectorBP': 2, + \ 'vimspectorBPCond': 3, + \ 'vimspectorBPDisabled': 4 + \ } + + " While not debugging + lcd testdata/cpp/simple + edit simple.cpp + + call setpos( '.', [ 0, 15, 1 ] ) + call vimspector#ToggleBreakpoint() + call vimspector#test#signs#AssertSignGroupSingletonAtLine( 'VimspectorBP', + \ 15, + \ 'vimspectorBP', + \ 2 ) + call setpos( '.', [ 0, 16, 1 ] ) + call vimspector#ToggleBreakpoint() + call vimspector#ToggleBreakpoint() + call vimspector#test#signs#AssertSignGroupSingletonAtLine( + \ 'VimspectorBP', + \ 16, + \ 'vimspectorBPDisabled', + \ 4 ) + call vimspector#ToggleBreakpoint() + call vimspector#test#signs#AssertSignGroupEmptyAtLine( 'VimspectorBP', 16 ) + + call setpos( '.', [ 0, 17, 1 ] ) + call vimspector#ToggleBreakpoint( { 'condition': '1' } ) + call vimspector#test#signs#AssertSignGroupSingletonAtLine( + \ 'VimspectorBP', + \ 17, + \ 'vimspectorBPCond', + \ 3 ) + + " While debugging + call vimspector#Launch() + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( 'simple.cpp', 15, 1 ) + call vimspector#test#signs#AssertPCIsAtLineInBuffer( 'simple.cpp', 15 ) + call vimspector#test#signs#AssertSignAtLine( + \ 'VimspectorCode', + \ 15, + \ 'vimspectorBP', + \ 2 ) + call vimspector#test#signs#AssertSignAtLine( + \ 'VimspectorCode', + \ 15, + \ 'vimspectorPCBP', + \ 1 ) + call vimspector#test#signs#AssertSignGroupSingletonAtLine( 'VimspectorCode', + \ 17, + \ 'vimspectorBP', + \ 2 ) + + call vimspector#StepOver() + " No sign as disabled + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( 'simple.cpp', 16, 1 ) + call vimspector#test#signs#AssertPCIsAtLineInBuffer( 'simple.cpp', 16 ) + + call vimspector#StepOver() + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( 'simple.cpp', 17, 1 ) + call vimspector#test#signs#AssertPCIsAtLineInBuffer( 'simple.cpp', 17 ) + + call vimspector#test#signs#AssertSignGroupSingletonAtLine( + \ 'VimspectorCode', + \ 15, + \ 'vimspectorBP', + \ 2 ) + call vimspector#test#signs#AssertSignAtLine( + \ 'VimspectorCode', + \ 17, + \ 'vimspectorBP', + \ 2 ) + call vimspector#test#signs#AssertSignAtLine( + \ 'VimspectorCode', + \ 17, + \ 'vimspectorPCBP', + \ 1 ) + + + call vimspector#test#setup#Reset() + lcd - + %bwipeout! + unlet! g:vimspector_sign_priority +endfunction + +function! Test_Custom_Breakpoint_Priority_Partial() + let g:vimspector_sign_priority = { + \ 'vimspectorBP': 2, + \ 'vimspectorBPCond': 3, + \ 'vimspectorBPDisabled': 4 + \ } + + " While not debugging + lcd testdata/cpp/simple + edit simple.cpp + + call setpos( '.', [ 0, 15, 1 ] ) + call vimspector#ToggleBreakpoint() + call vimspector#test#signs#AssertSignGroupSingletonAtLine( 'VimspectorBP', + \ 15, + \ 'vimspectorBP', + \ 2 ) + call setpos( '.', [ 0, 16, 1 ] ) + call vimspector#ToggleBreakpoint() + call vimspector#ToggleBreakpoint() + call vimspector#test#signs#AssertSignGroupSingletonAtLine( + \ 'VimspectorBP', + \ 16, + \ 'vimspectorBPDisabled', + \ 4 ) + call vimspector#ToggleBreakpoint() + call vimspector#test#signs#AssertSignGroupEmptyAtLine( 'VimspectorBP', 16 ) + + call setpos( '.', [ 0, 17, 1 ] ) + call vimspector#ToggleBreakpoint( { 'condition': '1' } ) + call vimspector#test#signs#AssertSignGroupSingletonAtLine( + \ 'VimspectorBP', + \ 17, + \ 'vimspectorBPCond', + \ 3 ) + + " While debugging + call vimspector#Launch() + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( 'simple.cpp', 15, 1 ) + call vimspector#test#signs#AssertPCIsAtLineInBuffer( 'simple.cpp', 15 ) + call vimspector#test#signs#AssertSignAtLine( + \ 'VimspectorCode', + \ 15, + \ 'vimspectorBP', + \ 2 ) + call vimspector#test#signs#AssertSignAtLine( + \ 'VimspectorCode', + \ 15, + \ 'vimspectorPCBP', + \ 200 ) + call vimspector#test#signs#AssertSignGroupSingletonAtLine( 'VimspectorCode', + \ 17, + \ 'vimspectorBP', + \ 2 ) + + call vimspector#StepOver() + " No sign as disabled + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( 'simple.cpp', 16, 1 ) + call vimspector#test#signs#AssertPCIsAtLineInBuffer( 'simple.cpp', 16 ) + + call vimspector#StepOver() + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( 'simple.cpp', 17, 1 ) + call vimspector#test#signs#AssertPCIsAtLineInBuffer( 'simple.cpp', 17 ) + + call vimspector#test#signs#AssertSignGroupSingletonAtLine( + \ 'VimspectorCode', + \ 15, + \ 'vimspectorBP', + \ 2 ) + call vimspector#test#signs#AssertSignAtLine( + \ 'VimspectorCode', + \ 17, + \ 'vimspectorBP', + \ 2 ) + call vimspector#test#signs#AssertSignAtLine( + \ 'VimspectorCode', + \ 17, + \ 'vimspectorPCBP', + \ 200 ) + + + call vimspector#test#setup#Reset() + lcd - + %bwipeout! + unlet! g:vimspector_sign_priority +endfunction + + +function! Test_Add_Line_BP_In_Other_File_While_Debugging() + call ThisTestIsFlaky() + let moo = 'moo.py' + let cow = 'cow.py' + lcd ../support/test/python/multiple_files + exe 'edit' moo + + call vimspector#Launch() + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( moo, 1, 1 ) + call vimspector#test#signs#AssertPCIsAtLineInBuffer( moo, 1 ) + + call cursor( 6, 3 ) + call vimspector#ToggleBreakpoint() + call vimspector#test#signs#AssertPCIsAtLineInBuffer( moo, 1 ) + call WaitForAssert( {-> + \vimspector#test#signs#AssertSignGroupSingletonAtLine( + \ 'VimspectorCode', + \ 6, + \ 'vimspectorBP', + \ 9 ) } ) + + exe 'edit' cow + call cursor( 2, 1 ) + call vimspector#ToggleBreakpoint() + call vimspector#test#signs#AssertSignGroupEmptyAtLine( 'VimspectorCode', 6 ) + call WaitForAssert( {-> + \ vimspector#test#signs#AssertSignGroupSingletonAtLine( + \ 'VimspectorCode', + \ 2, + \ 'vimspectorBP', + \ 9 ) } ) + + call vimspector#Continue() + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( moo, 6, 1 ) + call vimspector#test#signs#AssertPCIsAtLineInBuffer( moo, 6 ) + + call vimspector#test#signs#AssertSignGroupEmptyAtLine( 'VimspectorCode', 2 ) + call vimspector#test#signs#AssertSignAtLine( + \ 'VimspectorCode', + \ 6, + \ 'vimspectorBP', + \ 9 ) + call vimspector#test#signs#AssertSignAtLine( + \ 'VimspectorCode', + \ 6, + \ 'vimspectorPCBP', + \ 200 ) + + call vimspector#Continue() + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( cow, 2, 1 ) + call vimspector#test#signs#AssertPCIsAtLineInBuffer( cow, 2 ) + + call vimspector#test#signs#AssertSignGroupEmptyAtLine( 'VimspectorCode', 6 ) + call vimspector#test#signs#AssertSignAtLine( + \ 'VimspectorCode', + \ 2, + \ 'vimspectorBP', + \ 9 ) + call vimspector#test#signs#AssertSignAtLine( + \ 'VimspectorCode', + \ 2, + \ 'vimspectorPCBP', + \ 200 ) + + lcd - + call vimspector#test#setup#Reset() + %bwipe! +endfunction diff --git a/tests/ci/image/Dockerfile b/tests/ci/image/Dockerfile index 71329b6..164a5a7 100644 --- a/tests/ci/image/Dockerfile +++ b/tests/ci/image/Dockerfile @@ -4,9 +4,21 @@ ENV DEBIAN_FRONTEND=noninteractive ENV LC_ALL C.UTF-8 RUN apt-get update && \ + apt-get install -y curl \ + dirmngr \ + apt-transport-https \ + lsb-release \ + ca-certificates \ + software-properties-common && \ + curl -sL https://deb.nodesource.com/setup_12.x | bash - && \ + add-apt-repository ppa:bartbes/love-stable -y && \ + apt-get update && \ apt-get -y dist-upgrade && \ - apt-get -y install python3-dev \ + apt-get -y install gcc-8 \ + g++-8 \ + python3-dev \ python3-pip \ + python3-venv \ ca-cacert \ locales \ language-pack-en \ @@ -16,18 +28,22 @@ RUN apt-get update && \ tcllib \ gdb \ lldb \ - curl \ nodejs \ - npm && \ + lua5.1 \ + luajit \ + love && \ apt-get -y autoremove RUN ln -fs /usr/share/zoneinfo/Europe/London /etc/localtime && \ dpkg-reconfigure --frontend noninteractive tzdata +RUN update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-8 1 \ + --slave /usr/bin/g++ g++ /usr/bin/g++-8 + ## cleanup of files from setup RUN rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* -ARG VIM_VERSION=v8.1.1270 +ARG VIM_VERSION=v8.2.0716 ENV CONF_ARGS "--with-features=huge \ --enable-python3interp \ @@ -54,6 +70,12 @@ RUN mkdir -p /home/linuxbrew/.linuxbrew &&\ RUN /home/linuxbrew/.linuxbrew/bin/brew install golang +# dotnet +RUN curl -sSL https://dot.net/v1/dotnet-install.sh \ + | bash /dev/stdin --channel LTS --install-dir /usr/share/dotnet && \ + update-alternatives --install /usr/bin/dotnet dotnet \ + /usr/share/dotnet/dotnet 1 + # clean up RUN /home/linuxbrew/.linuxbrew/bin/brew cleanup && \ rm -rf ~/.cache && \ diff --git a/tests/get_configurations.test.vim b/tests/get_configurations.test.vim new file mode 100644 index 0000000..33e6577 --- /dev/null +++ b/tests/get_configurations.test.vim @@ -0,0 +1,22 @@ +function! SetUp() + call vimspector#test#setup#SetUpWithMappings( v:none ) +endfunction + +function! ClearDown() + call vimspector#test#setup#ClearDown() +endfunction + +function Test_Get_Configurations() + lcd ../support/test/csharp/ + + let configs = vimspector#GetConfigurations() + call assert_equal([ + \ 'launch - netcoredbg', + \ 'launch - netcoredbg - with debug log', + \ 'launch - mono', + \ ], configs) + + lcd - + %bwipe! +endfunction + diff --git a/tests/language_csharp.test.vim b/tests/language_csharp.test.vim new file mode 100644 index 0000000..64cf954 --- /dev/null +++ b/tests/language_csharp.test.vim @@ -0,0 +1,65 @@ +function! SetUp() + call vimspector#test#setup#SetUpWithMappings( v:none ) +endfunction + +function! ClearDown() + call vimspector#test#setup#ClearDown() +endfunction + +function! SetUp_Test_Go_Simple() + let g:vimspector_enable_mappings = 'HUMAN' +endfunction + +function! Test_CSharp_Simple() + let fn='Program.cs' + lcd ../support/test/csharp + exe 'edit ' . fn + + call vimspector#SetLineBreakpoint( fn, 31 ) + call vimspector#LaunchWithSettings( { + \ 'configuration': 'launch - netcoredbg' + \ } ) + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( fn, 31, 7 ) + call WaitForAssert( {-> + \ vimspector#test#signs#AssertPCIsAtLineInBuffer( fn, 31 ) + \ } ) + + call vimspector#StepOver() + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( fn, 32, 12 ) + call WaitForAssert( {-> + \ vimspector#test#signs#AssertPCIsAtLineInBuffer( fn, 32 ) + \ } ) + + call vimspector#test#setup#Reset() + + lcd - + %bwipeout! +endfunction + + +function! Test_Run_To_Cursor() + let fn='Program.cs' + lcd ../support/test/csharp + exe 'edit ' . fn + + call vimspector#SetLineBreakpoint( fn, 31 ) + call vimspector#LaunchWithSettings( { + \ 'configuration': 'launch - netcoredbg' + \ } ) + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( fn, 31, 7 ) + call WaitForAssert( {-> + \ vimspector#test#signs#AssertPCIsAtLineInBuffer( fn, 31 ) + \ } ) + + call cursor( 33, 1 ) + call vimspector#RunToCursor() + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( fn, 33, 1 ) + call WaitForAssert( {-> + \ vimspector#test#signs#AssertPCIsAtLineInBuffer( fn, 33 ) + \ } ) + + call vimspector#test#setup#Reset() + lcd - + %bwipeout! +endfunction + diff --git a/tests/language_go.test.vim b/tests/language_go.test.vim index 7fb9c9d..200d5d6 100644 --- a/tests/language_go.test.vim +++ b/tests/language_go.test.vim @@ -23,7 +23,8 @@ function! Test_Go_Simple() call feedkeys( "\", 'xt' ) call vimspector#test#signs#AssertSignGroupSingletonAtLine( 'VimspectorBP', \ 4, - \ 'vimspectorBP' ) + \ 'vimspectorBP', + \ 9 ) call setpos( '.', [ 0, 1, 1 ] ) @@ -44,3 +45,28 @@ function! Test_Go_Simple() lcd - %bwipeout! endfunction + + +function! Test_Run_To_Cursor() + let fn='hello-world.go' + lcd ../support/test/go/hello_world + exe 'edit ' . fn + + call vimspector#SetLineBreakpoint( fn, 4 ) + call vimspector#Launch() + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( fn, 4, 1 ) + call WaitForAssert( {-> + \ vimspector#test#signs#AssertPCIsAtLineInBuffer( fn, 4 ) + \ } ) + + call cursor( 5, 1 ) + call vimspector#RunToCursor() + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( fn, 5, 1 ) + call WaitForAssert( {-> + \ vimspector#test#signs#AssertPCIsAtLineInBuffer( fn, 5 ) + \ } ) + + call vimspector#test#setup#Reset() + lcd - + %bwipeout! +endfunction diff --git a/tests/language_lua.test.vim b/tests/language_lua.test.vim new file mode 100644 index 0000000..dc296bb --- /dev/null +++ b/tests/language_lua.test.vim @@ -0,0 +1,74 @@ +function! SetUp() + let g:vimspector_enable_mappings = 'HUMAN' + call vimspector#test#setup#SetUpWithMappings( v:none ) +endfunction + +function! ClearDown() + call vimspector#test#setup#ClearDown() +endfunction + + +function! BaseTest( configuration ) + let fn='simple.lua' + lcd ../support/test/lua/simple + exe 'edit ' . fn + + call vimspector#SetLineBreakpoint( fn, 5 ) + call vimspector#LaunchWithSettings( { 'configuration': a:configuration } ) + + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( fn, 5, 1 ) + call WaitForAssert( {-> + \ vimspector#test#signs#AssertPCIsAtLineInBuffer( fn, 5 ) + \ } ) + + " Step + call feedkeys( "\", 'xt' ) + + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( fn, 6, 1 ) + call WaitForAssert( {-> + \ vimspector#test#signs#AssertPCIsAtLineInBuffer( fn, 6 ) + \ } ) + + call vimspector#test#setup#Reset() + + lcd - + %bwipeout! +endfunction + + +function! Test_Lua_Simple() + call BaseTest( 'lua' ) +endfunction + + +function! Test_Lua_Luajit() + call BaseTest( 'luajit' ) +endfunction + + +function! Test_Lua_Love() + let fn='main.lua' + lcd ../support/test/lua/love-headless + exe 'edit ' . fn + + call vimspector#SetLineBreakpoint( fn, 8 ) + call vimspector#LaunchWithSettings( { 'configuration': 'love' } ) + + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( fn, 8, 1 ) + call WaitForAssert( {-> + \ vimspector#test#signs#AssertPCIsAtLineInBuffer( fn, 8 ) + \ } ) + + " Step + call feedkeys( "\", 'xt' ) + + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( fn, 9, 1 ) + call WaitForAssert( {-> + \ vimspector#test#signs#AssertPCIsAtLineInBuffer( fn, 9 ) + \ } ) + + call vimspector#test#setup#Reset() + + lcd - + %bwipeout! +endfunction diff --git a/tests/language_python.test.vim b/tests/language_python.test.vim index 5240fac..cc49adb 100644 --- a/tests/language_python.test.vim +++ b/tests/language_python.test.vim @@ -6,11 +6,11 @@ function! ClearDown() call vimspector#test#setup#ClearDown() endfunction -function! SetUp_Test_Go_Simple() +function! SetUp_Test_Python_Simple() let g:vimspector_enable_mappings = 'HUMAN' endfunction -function! Test_Go_Simple() +function! Test_Python_Simple() let fn='main.py' lcd ../support/test/python/simple_python exe 'edit ' . fn @@ -23,21 +23,14 @@ function! Test_Go_Simple() call feedkeys( "\", 'xt' ) call vimspector#test#signs#AssertSignGroupSingletonAtLine( 'VimspectorBP', \ 6, - \ 'vimspectorBP' ) + \ 'vimspectorBP', + \ 9 ) call setpos( '.', [ 0, 1, 1 ] ) " Here we go. Start Debugging - pyx << EOF -from unittest.mock import patch -with patch( 'vimspector.utils.SelectFromList', - return_value=None ) as p: - with patch( 'vimspector.utils.AskForInput', - return_value=None ) as p: - vim.eval( 'vimspector#LaunchWithSettings( { "configuration": "run" } )' ) - vim.eval( 'vimspector#test#signs#AssertCursorIsAtLineInBuffer( fn, 6, 1 )' ) - p.assert_called() -EOF + call vimspector#LaunchWithSettings( { 'configuration': 'run' } ) + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( fn, 6, 1 ) " Step call feedkeys( "\", 'xt' ) @@ -53,3 +46,64 @@ EOF %bwipeout! endfunction +function! SetUp_Test_Python_Remote_Attach() + let g:vimspector_enable_mappings = 'HUMAN' +endfunction + +function! Test_Python_Remote_Attach() + lcd ../support/test/python/simple_python + let fn='main.py' + exe 'edit ' . fn + + let ready = v:false + function! ReceiveFromLauncher( ch, data ) closure + if a:data ==# '*** Launching ***' + let ready = v:true + endif + endfunction + + let jobid = job_start( [ './run_with_debugpy' ], { + \ 'out_mode': 'nl', + \ 'out_cb': funcref( 'ReceiveFromLauncher' ), + \ } ) + + " Wait up to 60s for the debugee to be launched (the script faffs with + " virtualenvs etc.) + call WaitFor( {-> ready == v:true }, 60000 ) + + call setpos( '.', [ 0, 6, 1 ] ) + + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( fn, 6, 1 ) + call vimspector#test#signs#AssertSignGroupEmptyAtLine( 'VimspectorBP', 6 ) + + " Add the breakpoint + call feedkeys( "\", 'xt' ) + call vimspector#test#signs#AssertSignGroupSingletonAtLine( 'VimspectorBP', + \ 6, + \ 'vimspectorBP', + \ 9 ) + + call setpos( '.', [ 0, 1, 1 ] ) + + " Here we go. Start Debugging (note will wait up to 10s for the script to do + " its virtualenv thing) + call vimspector#LaunchWithSettings( { + \ 'configuration': 'attach', + \ 'port': 5678, + \ 'host': 'localhost' + \ } ) + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( fn, 6, 1 ) + + " Step + call feedkeys( "\", 'xt' ) + + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( fn, 7, 1 ) + call WaitForAssert( {-> + \ vimspector#test#signs#AssertPCIsAtLineInBuffer( fn, 7 ) + \ } ) + + call vimspector#test#setup#Reset() + call job_stop( jobid ) + lcd - + %bwipeout! +endfunction diff --git a/tests/lib/autoload/vimspector/test/setup.vim b/tests/lib/autoload/vimspector/test/setup.vim index a1df8d5..6ab220c 100644 --- a/tests/lib/autoload/vimspector/test/setup.vim +++ b/tests/lib/autoload/vimspector/test/setup.vim @@ -11,26 +11,98 @@ function! vimspector#test#setup#SetUpWithMappings( mappings ) abort " This is a bit of a hack runtime! plugin/**/*.vim + + augroup VimspectorTestSwap + au! + au SwapExists * let v:swapchoice = 'e' + augroup END + endfunction function! vimspector#test#setup#ClearDown() abort endfunction -function! vimspector#test#setup#Reset() abort - call vimspector#Reset() +function! vimspector#test#setup#WaitForReset() abort + call WaitForAssert( {-> assert_equal( 1, len( gettabinfo() ) ) } ) call WaitForAssert( {-> - \ assert_true( pyxeval( '_vimspector_session._connection is None' ) ) + \ assert_true( pyxeval( '_vimspector_session is None or ' . + \ '_vimspector_session._connection is None' ) ) \ } ) call WaitForAssert( {-> - \ assert_true( pyxeval( '_vimspector_session._uiTab is None' ) ) + \ assert_true( pyxeval( '_vimspector_session is None or ' . + \ '_vimspector_session._uiTab is None' ) ) \ }, 10000 ) call vimspector#test#signs#AssertSignGroupEmpty( 'VimspectorCode' ) +endfunction + +function! vimspector#test#setup#Reset() abort + call vimspector#Reset() + call vimspector#test#setup#WaitForReset() + call vimspector#ClearBreakpoints() call vimspector#test#signs#AssertSignGroupEmpty( 'VimspectorBP' ) if exists( '*vimspector#internal#state#Reset' ) call vimspector#internal#state#Reset() endif + + call popup_clear() endfunction +let s:g_stack = {} + +function! vimspector#test#setup#PushGlobal( name, value ) abort + if !has_key( s:g_stack, a:name ) + let s:g_stack[ a:name ] = [] + endif + + let old_value = get( g:, a:name, v:null ) + call add( s:g_stack[ a:name ], old_value ) + let g:[ a:name ] = a:value + + return old_value +endfunction + +function! vimspector#test#setup#PopGlobal( name ) abort + if !has_key( s:g_stack, a:name ) || len( s:g_stack[ a:name ] ) == 0 + return v:null + endif + + let old_value = s:g_stack[ a:name ][ -1 ] + call remove( s:g_stack[ a:name ], -1 ) + + if old_value is v:null + silent! call remove( g:, a:name ) + else + let g:[ a:name ] = old_value + endif + + return old_value +endfunction + +let s:o_stack = {} + +function! vimspector#test#setup#PushOption( name, value ) abort + if !has_key( s:o_stack, a:name ) + let s:o_stack[ a:name ] = [] + endif + + let old_value = v:null + execute 'let old_value = &' . a:name + call add( s:o_stack[ a:name ], old_value ) + execute 'set ' . a:name . '=' . a:value + return old_value +endfunction + +function! vimspector#test#setup#PopOption( name ) abort + if !has_key( s:o_stack, a:name ) || len( s:o_stack[ a:name ] ) == 0 + return v:null + endif + + let old_value = s:o_stack[ a:name ][ -1 ] + call remove( s:o_stack[ a:name ], -1 ) + + execute 'set ' . a:name . '=' . old_value + return old_value +endfunction diff --git a/tests/lib/autoload/vimspector/test/signs.vim b/tests/lib/autoload/vimspector/test/signs.vim index 1ef7fc9..cf64520 100644 --- a/tests/lib/autoload/vimspector/test/signs.vim +++ b/tests/lib/autoload/vimspector/test/signs.vim @@ -1,16 +1,22 @@ function! vimspector#test#signs#AssertCursorIsAtLineInBuffer( buffer, \ line, \ column ) abort + call WaitFor( {-> bufexists( a:buffer ) } ) call WaitForAssert( {-> - \ assert_equal( a:buffer, bufname( '%' ), 'Current buffer' ) + \ assert_equal( fnamemodify( a:buffer, ':p' ), + \ fnamemodify( bufname( '%' ), ':p' ), + \ 'Current buffer' ) \ }, 10000 ) call WaitForAssert( {-> \ assert_equal( a:line, line( '.' ), 'Current line' ) \ }, 10000 ) - call assert_equal( a:column, col( '.' ), 'Current column' ) + if a:column isnot v:null + call assert_equal( a:column, col( '.' ), 'Current column' ) + endif endfunction function! vimspector#test#signs#AssertPCIsAtLineInBuffer( buffer, line ) abort + call WaitFor( {-> bufexists( a:buffer ) } ) let signs = sign_getplaced( a:buffer, { \ 'group': 'VimspectorCode', \ } ) @@ -28,7 +34,7 @@ function! vimspector#test#signs#AssertPCIsAtLineInBuffer( buffer, line ) abort let index = 0 while index < len( signs[ 0 ].signs ) let s = signs[ 0 ].signs[ index ] - if s.name ==# 'vimspectorPC' + if s.name ==# 'vimspectorPC' || s.name ==# 'vimspectorPCBP' if assert_false( pc_index >= 0, 'Too many PC signs' ) return 1 endif @@ -42,7 +48,8 @@ endfunction function! vimspector#test#signs#AssertSignGroupSingletonAtLine( group, \ line, - \ sign_name ) + \ sign_name, + \ priority ) \ abort let signs = sign_getplaced( '%', { @@ -58,14 +65,54 @@ function! vimspector#test#signs#AssertSignGroupSingletonAtLine( group, \ 'Num signs in ' . a:group . ' at ' . a:line ) || \ assert_equal( a:sign_name, \ signs[ 0 ].signs[ 0 ].name, - \ 'Sign in group ' . a:group . ' at ' . a:line ) + \ 'Sign in group ' . a:group . ' at ' . a:line ) || + \ assert_equal( a:priority, + \ signs[ 0 ].signs[ 0 ].priority, + \ 'Sign priority in group ' . a:group . ' at ' . a:line ) endfunction +function! vimspector#test#signs#AssertSignAtLine( + \ group, + \ line, + \ sign_name, + \ priority ) abort + + let signs = sign_getplaced( '%', { + \ 'group': a:group, + \ 'lnum': a:line, + \ } ) + + let errors_before = v:errors + let result = 1 + let errors = [ 'No signs were found' ] + + for sign in signs[ 0 ].signs + let v:errors = [] + + let result = + \ assert_equal( a:sign_name, + \ sign.name, + \ 'Sign in group ' . a:group . ' at ' . a:line ) || + \ assert_equal( a:priority, + \ sign.priority, + \ 'Sign priority in group ' . a:group . ' at ' . a:line ) + if result + let errors = v:errors + else + let errors = [] + break + endif + endfor + + let v:errors = errors_before + errors + return result +endfunction + function! vimspector#test#signs#AssertSignGroupEmptyAtLine( group, line ) abort let signs = sign_getplaced( '%', { \ 'group': a:group, - \ 'lnum': line( '.' ) + \ 'lnum': a:line, \ } ) return assert_equal( 1, diff --git a/tests/lib/plugin/screendump.vim b/tests/lib/plugin/screendump.vim index 1c710d7..157febd 100644 --- a/tests/lib/plugin/screendump.vim +++ b/tests/lib/plugin/screendump.vim @@ -28,7 +28,7 @@ endif func RunVimInTerminal(arguments, options) " If Vim doesn't exit a swap file remains, causing other tests to fail. " Remove it here. - call delete(".swp") + call delete('.swp') if exists('$COLORFGBG') " Clear $COLORFGBG to avoid 'background' being set to "dark", which will @@ -61,7 +61,7 @@ func RunVimInTerminal(arguments, options) \ 'term_rows': rows, \ 'term_cols': cols, \ }) - if &termwinsize == '' + if &termwinsize ==# '' " in the GUI we may end up with a different size, try to set it. if term_getsize(buf) != [rows, cols] call term_setsize(buf, rows, cols) @@ -80,7 +80,7 @@ func RunVimInTerminal(arguments, options) call WaitFor({-> len(term_getline(buf, rows)) >= cols - 1 || len(term_getline(buf, rows - statusoff)) >= cols - 1}) catch /timed out after/ let lines = map(range(1, rows), {key, val -> term_getline(buf, val)}) - call assert_report('RunVimInTerminal() failed, screen contents: ' . join(lines, "")) + call assert_report('RunVimInTerminal() failed, screen contents: ' . join(lines, '')) endtry return buf @@ -88,13 +88,13 @@ endfunc " Stop a Vim running in terminal buffer "buf". func StopVimInTerminal(buf) - call assert_equal("running", term_getstatus(a:buf)) + call assert_equal('running', term_getstatus(a:buf)) " CTRL-O : works both in Normal mode and Insert mode to start a command line. " In Command-line it's inserted, the CTRL-U removes it again. call term_sendkeys(a:buf, "\:\qa!\") - call WaitForAssert({-> assert_equal("finished", term_getstatus(a:buf))}) + call WaitForAssert({-> assert_equal('finished', term_getstatus(a:buf))}) only! endfunc diff --git a/tests/lib/plugin/shared.vim b/tests/lib/plugin/shared.vim index b6f0205..f98b8e9 100644 --- a/tests/lib/plugin/shared.vim +++ b/tests/lib/plugin/shared.vim @@ -6,115 +6,6 @@ if exists('*WaitFor') finish endif -" Get the name of the Python executable. -" Also keeps it in s:python. -func PythonProg() - " This test requires the Python command to run the test server. - " This most likely only works on Unix and Windows. - if has('unix') - " We also need the job feature or the pkill command to make sure the server - " can be stopped. - if !(executable('python') && (has('job') || executable('pkill'))) - return '' - endif - let s:python = 'python' - elseif has('win32') - " Use Python Launcher for Windows (py.exe) if available. - if executable('py.exe') - let s:python = 'py.exe' - elseif executable('python.exe') - let s:python = 'python.exe' - else - return '' - endif - else - return '' - endif - return s:python -endfunc - -" Run "cmd". Returns the job if using a job. -func RunCommand(cmd) - let job = 0 - if has('job') - let job = job_start(a:cmd, {"stoponexit": "hup"}) - call job_setoptions(job, {"stoponexit": "kill"}) - elseif has('win32') - exe 'silent !start cmd /c start "test_channel" ' . a:cmd - else - exe 'silent !' . a:cmd . '&' - endif - return job -endfunc - -" Read the port number from the Xportnr file. -func GetPort() - let l = [] - " with 200 it sometimes failed - for i in range(400) - try - let l = readfile("Xportnr") - catch - endtry - if len(l) >= 1 - break - endif - sleep 10m - endfor - call delete("Xportnr") - - if len(l) == 0 - " Can't make the connection, give up. - return 0 - endif - return l[0] -endfunc - -" Run a Python server for "cmd" and call "testfunc". -" Always kills the server before returning. -func RunServer(cmd, testfunc, args) - " The Python program writes the port number in Xportnr. - call delete("Xportnr") - - if len(a:args) == 1 - let arg = ' ' . a:args[0] - else - let arg = '' - endif - let pycmd = s:python . " " . a:cmd . arg - - try - let g:currentJob = RunCommand(pycmd) - - " Wait for up to 2 seconds for the port number to be there. - let port = GetPort() - if port == 0 - call assert_false(1, "Can't start " . a:cmd) - return - endif - - call call(function(a:testfunc), [port]) - catch - call assert_false(1, 'Caught exception: "' . v:exception . '" in ' . v:throwpoint) - finally - call s:kill_server(a:cmd) - endtry -endfunc - -func s:kill_server(cmd) - if has('job') - if exists('g:currentJob') - call job_stop(g:currentJob) - unlet g:currentJob - endif - elseif has('win32') - let cmd = substitute(a:cmd, ".py", '', '') - call system('taskkill /IM ' . s:python . ' /T /F /FI "WINDOWTITLE eq ' . cmd . '"') - else - call system("pkill -f " . a:cmd) - endif -endfunc - " Wait for up to five seconds for "expr" to become true. "expr" can be a " stringified expression to evaluate, or a funcref without arguments. " Using a lambda works best. Example: @@ -160,7 +51,10 @@ func s:WaitForCommon(expr, assert, timeout) let start = reltime() endif + let iters = 0 + while 1 + let iters += 1 let errors_before = len( v:errors ) if type(a:expr) == v:t_func let success = a:expr() @@ -174,13 +68,20 @@ func s:WaitForCommon(expr, assert, timeout) return slept endif + if iters % 20 == 0 + redraw! + endif + if slept >= a:timeout break endif if type(a:assert) == v:t_func " Remove the errors added by the assert function. - call remove(v:errors, -1 * len( v:errors ) - errors_before ) + let errors_added = len( v:errors ) - errors_before + if errors_added > 0 + call remove( v:errors, -1 * errors_added, -1 ) + endif endif sleep 10m @@ -194,169 +95,34 @@ func s:WaitForCommon(expr, assert, timeout) return -1 " timed out endfunc +function! ThisTestIsFlaky() + let g:test_is_flaky = v:true +endfunction -" Wait for up to a given milliseconds. -" With the +timers feature this waits for key-input by getchar(), Resume() -" feeds key-input and resumes process. Return time waited in milliseconds. -" Without +timers it uses simply :sleep. -func Standby(msec) - if has('timers') - let start = reltime() - let g:_standby_timer = timer_start(a:msec, function('s:feedkeys')) - call getchar() - return float2nr(reltimefloat(reltime(start)) * 1000) +function! AssertMatchList( expected, actual ) abort + let ret = assert_equal( len( a:expected ), len( a:actual ) ) + let len = min( [ len( a:expected ), len( a:actual ) ] ) + let idx = 0 + while idx < len + let ret += assert_match( a:expected[ idx ], a:actual[ idx ] ) + let idx += 1 + endwhile + return ret +endfunction + + +function! GetBufLine( buf, start, end = '$' ) + if type( a:start ) != v:t_string && a:start < 0 + let start = getbufinfo( a:buf )[ 0 ].linecount + a:start else - execute 'sleep ' a:msec . 'm' - return a:msec + let start = a:start endif -endfunc -func Resume() - if exists('g:_standby_timer') - call timer_stop(g:_standby_timer) - call s:feedkeys(0) - unlet g:_standby_timer - endif -endfunc - -func s:feedkeys(timer) - call feedkeys('x', 'nt') -endfunc - -" Get $VIMPROG to run Vim executable. -" The Makefile writes it as the first line in the "vimcmd" file. -func GetVimProg() - if !filereadable('vimcmd') - " Assume the script was sourced instead of running "make". - return '../vim' - endif - return readfile('vimcmd')[0] -endfunc - -let g:valgrind_cnt = 1 - -" Get the command to run Vim, with -u NONE and --not-a-term arguments. -" If there is an argument use it instead of "NONE". -func GetVimCommand(...) - if !filereadable('vimcmd') - echo 'Cannot read the "vimcmd" file, falling back to ../vim.' - let lines = ['../vim'] + if type( a:end ) != v:t_string && a:end < 0 + let end = getbufinfo( a:buf )[ 0 ].linecount + a:end else - let lines = readfile('vimcmd') - endif - if a:0 == 0 - let name = 'NONE' - else - let name = a:1 - endif - " For Unix Makefile writes the command to use in the second line of the - " "vimcmd" file, including environment options. - " Other Makefiles just write the executable in the first line, so fall back - " to that if there is no second line or it is empty. - if len(lines) > 1 && lines[1] != '' - let cmd = lines[1] - else - let cmd = lines[0] + let end = a:end endif - let cmd = substitute(cmd, '-u \f\+', '-u ' . name, '') - if cmd !~ '-u '. name - let cmd = cmd . ' -u ' . name - endif - let cmd .= ' --not-a-term' - let cmd = substitute(cmd, 'VIMRUNTIME=.*VIMRUNTIME;', '', '') - - " If using valgrind, make sure every run uses a different log file. - if cmd =~ 'valgrind.*--log-file=' - let cmd = substitute(cmd, '--log-file=\(^\s*\)', '--log-file=\1.' . g:valgrind_cnt, '') - let g:valgrind_cnt += 1 - endif - - return cmd -endfunc - -" Get the command to run Vim, with --clean. -func GetVimCommandClean() - let cmd = GetVimCommand() - let cmd = substitute(cmd, '-u NONE', '--clean', '') - let cmd = substitute(cmd, '--not-a-term', '', '') - - " Optionally run Vim under valgrind - " let cmd = 'valgrind --tool=memcheck --leak-check=yes --num-callers=25 --log-file=valgrind ' . cmd - - return cmd -endfunc - -" Run Vim, using the "vimcmd" file and "-u NORC". -" "before" is a list of Vim commands to be executed before loading plugins. -" "after" is a list of Vim commands to be executed after loading plugins. -" Plugins are not loaded, unless 'loadplugins' is set in "before". -" Return 1 if Vim could be executed. -func RunVim(before, after, arguments) - return RunVimPiped(a:before, a:after, a:arguments, '') -endfunc - -func RunVimPiped(before, after, arguments, pipecmd) - let cmd = GetVimCommand() - let args = '' - if len(a:before) > 0 - call writefile(a:before, 'Xbefore.vim') - let args .= ' --cmd "so Xbefore.vim"' - endif - if len(a:after) > 0 - call writefile(a:after, 'Xafter.vim') - let args .= ' -S Xafter.vim' - endif - - exe "silent !" . a:pipecmd . cmd . args . ' ' . a:arguments - - if len(a:before) > 0 - call delete('Xbefore.vim') - endif - if len(a:after) > 0 - call delete('Xafter.vim') - endif - return 1 -endfunc - -func CanRunGui() - return has('gui') && ($DISPLAY != "" || has('gui_running')) -endfunc - -func WorkingClipboard() - if !has('clipboard') - return 0 - endif - if has('x11') - return $DISPLAY != "" - endif - return 1 -endfunc - -" Get line "lnum" as displayed on the screen. -" Trailing white space is trimmed. -func! Screenline(lnum) - let chars = [] - for c in range(1, winwidth(0)) - call add(chars, nr2char(screenchar(a:lnum, c))) - endfor - let line = join(chars, '') - return matchstr(line, '^.\{-}\ze\s*$') -endfunc - -" Stops the shell running in terminal "buf". -func Stop_shell_in_terminal(buf) - call term_sendkeys(a:buf, "exit\r") - let job = term_getjob(a:buf) - call WaitFor({-> job_status(job) == "dead"}) -endfunc - -" Gets the text of a terminal line, using term_scrape() -func Get_terminal_text(bufnr, row) - let list = term_scrape(a:bufnr, a:row) - let text = '' - for item in list - let text .= item.chars - endfor - return text -endfunc + return getbufline( a:buf, start, end ) +endfunction diff --git a/tests/lib/run_test.vim b/tests/lib/run_test.vim index f8cef56..b2f1095 100644 --- a/tests/lib/run_test.vim +++ b/tests/lib/run_test.vim @@ -4,7 +4,7 @@ " " To execute only specific test functions, add a second argument. It will be " matched against the names of the Test_ funtion. E.g.: -" ../vim -u NONE -S runtest.vim test_channel.vim open_delay +" ../vim -Nu NONE vimrc -S lib/run_test.vim test_channel.vim open_delay " The output can be found in the "messages" file. " " The test script may contain anything, only functions that start with @@ -26,32 +26,72 @@ " It will be called after each Test_ function. " " When debugging a test it can be useful to add messages to v:errors: -" call add(v:errors, "this happened") +" call add(v:errors, "this happened") +" +" But for real debug logging: +" call ch_log( ",,,message..." ) +" Then view it in 'debuglog' -set rtp=$PWD/lib,$VIM/vimfiles,$VIMRUNTIME,$VIM/vimfiles/after +" Let a test take up to 1 minute, unless debugging +let s:single_test_timeout = 60000 + +" Restrict the runtimepath to the exact minimum needed for testing +let &runtimepath = getcwd() . '/lib' +set runtimepath+=$VIM/vimfiles,$VIMRUNTIME,$VIM/vimfiles/after if has('packages') - let &packpath = &rtp + let &packpath = &runtimepath endif call ch_logfile( 'debuglog', 'w' ) " For consistency run all tests with 'nocompatible' set. " This also enables use of line continuation. -set nocp viminfo+=nviminfo +" vint: Workaround for https://github.com/Vimjas/vint/issues/363 +" vint: -ProhibitSetNoCompatible +set nocompatible +" vint: +ProhibitSetNoCompatible " Use utf-8 by default, instead of whatever the system default happens to be. " Individual tests can overrule this at the top of the file. +" vint: -ProhibitEncodingOptionAfterScriptEncoding set encoding=utf-8 +" vint: +ProhibitEncodingOptionAfterScriptEncoding " Avoid stopping at the "hit enter" prompt set nomore " Output all messages in English. -lang mess C +lang messages C " Always use forward slashes. set shellslash +func s:TestFailed() + let log = readfile( expand( '~/.vimspector.log' ) ) + let logfile = s:testid_filesafe . '_vimspector.log.testlog' + call writefile( log, logfile, 's' ) + call add( s:messages, 'Wrote log for failed test: ' . logfile ) +endfunc + +func! Abort( timer_id ) + if exists( '&debugfunc' ) && &debugfunc !=# '' + return + endif + + call assert_report( 'Test timed out!!!' ) + qa! +endfunc + +func! TestLog( msg ) + if type( a:msg ) == v:t_string + let msg = [ a:msg ] + else + let msg = a:msg + endif + + call extend( s:messages, msg ) +endfunc + func RunTheTest(test) echo 'Executing ' . a:test @@ -73,7 +113,7 @@ func RunTheTest(test) " directory after executing the test. let save_cwd = getcwd() - if exists("*SetUp_" . a:test) + if exists('*SetUp_' . a:test) try exe 'call SetUp_' . a:test catch @@ -89,7 +129,7 @@ func RunTheTest(test) endtry endif - if exists("*SetUp") + if exists('*SetUp') try call SetUp() catch @@ -107,46 +147,69 @@ func RunTheTest(test) call add(s:messages, 'Executing ' . a:test) let s:done += 1 + let timer = timer_start( s:single_test_timeout, funcref( 'Abort' ) ) - if a:test =~ 'Test_nocatch_' - " Function handles errors itself. This avoids skipping commands after the - " error. - exe 'call ' . a:test - else - try - let s:test = a:test - let s:testid = g:testpath . ':' . a:test - let test_filesafe = substitute( a:test, ')', '_', 'g' ) - let test_filesafe = substitute( test_filesafe, '(', '_', 'g' ) - let test_filesafe = substitute( test_filesafe, ',', '_', 'g' ) - let test_filesafe = substitute( test_filesafe, ':', '_', 'g' ) - let s:testid_filesafe = g:testpath . '_' . test_filesafe + try + let s:test = a:test + let s:testid = g:testpath . ':' . a:test + + let test_filesafe = substitute( a:test, '[)(,:]', '_', 'g' ) + let s:testid_filesafe = g:testpath . '_' . test_filesafe + + augroup EarlyExit + au! au VimLeavePre * call EarlyExit(s:test) - exe 'call ' . a:test - au! VimLeavePre - catch /^\cskipped/ - call add(s:messages, ' Skipped') - call add(s:skipped, - \ 'SKIPPED ' . a:test - \ . ': ' - \ . substitute(v:exception, '^\S*\s\+', '', '')) - catch + augroup END + + exe 'call ' . a:test + catch /^\cskipped/ + call add(s:messages, ' Skipped') + call add(s:skipped, + \ 'SKIPPED ' . a:test + \ . ': ' + \ . substitute(v:exception, '^\S*\s\+', '', '')) + catch /^\cxfail/ + if len( v:errors ) == 0 call add(v:errors, - \ 'Caught exception in ' . a:test + \ 'Expected failure but no error in ' . a:test \ . ': ' \ . v:exception \ . ' @ ' \ . g:testpath \ . ':' \ . v:throwpoint) - endtry - endif + + call s:TestFailed() + else + let v:errors = [] + call add(s:messages, ' XFAIL' ) + call add(s:skipped, + \ 'XFAIL ' . a:test + \ . ': ' + \ . substitute(v:exception, '^\S*\s\+', '', '')) + endif + catch + call add(v:errors, + \ 'Caught exception in ' . a:test + \ . ': ' + \ . v:exception + \ . ' @ ' + \ . g:testpath + \ . ':' + \ . v:throwpoint) + + call s:TestFailed() + endtry + + au! EarlyExit + + call timer_stop( timer ) " In case 'insertmode' was set and something went wrong, make sure it is " reset to avoid trouble with anything else. set noinsertmode - if exists("*TearDown") + if exists('*TearDown') try call TearDown() catch @@ -161,7 +224,7 @@ func RunTheTest(test) endtry endif - if exists("*TearDown_" . a:test) + if exists('*TearDown_' . a:test) try exe 'call TearDown_' . a:test catch @@ -203,24 +266,16 @@ endfunc func AfterTheTest() if len(v:errors) > 0 let s:fail += 1 + call s:TestFailed() call add(s:errors, 'Found errors in ' . s:testid . ':') call extend(s:errors, v:errors) let v:errors = [] - - let log = readfile( expand( '~/.vimspector.log' ) ) - let logfile = s:testid_filesafe . '.vimspector.log' - call writefile( log, logfile, 's' ) - call add( s:messages, 'Wrote log for failed test: ' . logfile ) - call extend( s:messages, log ) endif endfunc func EarlyExit(test) " It's OK for the test we use to test the quit detection. - if a:test != 'Test_zz_quit_detected()' - call add(v:errors, 'Test caused Vim to exit: ' . a:test) - endif - + call add(v:errors, 'Test caused Vim to exit: ' . a:test) call FinishTesting() endfunc @@ -233,17 +288,18 @@ func FinishTesting() if s:fail == 0 " Success, create the .res file so that make knows it's done. - exe 'split ' . fnamemodify(g:testname, ':r') . '.res' - write + call writefile( [], g:testname . '.res', 's' ) endif if len(s:errors) > 0 " Append errors to test.log - split test.log - call append(line('$'), '') - call append(line('$'), 'From ' . g:testpath . ':') - call append(line('$'), s:errors) - write + let l = [] + if filereadable( 'test.log' ) + let l = readfile( 'test.log' ) + endif + call extend( l, [ '', 'From ' . g:testpath . ':' ] ) + call extend( l, s:errors ) + call writefile( l, 'test.log', 's' ) endif if s:done == 0 @@ -264,11 +320,13 @@ func FinishTesting() call extend(s:messages, s:skipped) " Append messages to the file "messages" - split messages - call append(line('$'), '') - call append(line('$'), 'From ' . g:testpath . ':') - call append(line('$'), s:messages) - write + let l = [] + if filereadable( 'messages' ) + let l = readfile( 'messages' ) + endif + call extend( l, [ '', 'From ' . g:testpath . ':' ] ) + call extend( l, s:messages ) + call writefile( l, 'messages', 's' ) if s:fail > 0 cquit! @@ -296,12 +354,6 @@ catch \ ' @ ' . v:throwpoint) endtry -" Names of flaky tests. -let s:flaky_tests = [] - -" Pattern indicating a common flaky test failure. -let s:flaky_errors_re = '__does_not_match__' - " Locate Test_ functions and execute them. redir @q silent function /^Test_ @@ -317,7 +369,34 @@ endif for s:test in sort(s:tests) " Silence, please! set belloff=all + + " A test can set g:test_is_flaky to retry running the test. + let g:test_is_flaky = 0 + call RunTheTest(s:test) + + " Repeat a flaky test. Give up when: + " - $TEST_NO_RETRY is not empty + " - it fails five times + if len(v:errors) > 0 + \ && $TEST_NO_RETRY == '' + \ && g:test_is_flaky + for retry in range( 10 ) + call add( s:messages, 'Found errors in ' . s:test . '. Retrying.' ) + call extend( s:messages, v:errors ) + + sleep 2 + + let v:errors = [] + call RunTheTest(s:test) + + if len(v:errors) == 0 + " Test passed on rerun. + break + endif + endfor + endif + call AfterTheTest() endfor diff --git a/tests/manual/image/Dockerfile b/tests/manual/image/Dockerfile index badfa36..7658ada 100644 --- a/tests/manual/image/Dockerfile +++ b/tests/manual/image/Dockerfile @@ -16,5 +16,10 @@ WORKDIR /home/dev ENV HOME /home/dev ENV PYTHON_CONFIGURE_OPTS --enable-shared +ENV VIMSPECTOR_MIMODE gdb +ENV GOPATH /home/dev/go + +RUN eval $(/home/linuxbrew/.linuxbrew/bin/brew shellenv) && \ + go get -u github.com/go-delve/delve/cmd/dlv ADD --chown=dev:dev .vim/ /home/dev/.vim/ diff --git a/tests/mappings.test.vim b/tests/mappings.test.vim new file mode 100644 index 0000000..b0e288c --- /dev/null +++ b/tests/mappings.test.vim @@ -0,0 +1,143 @@ +function! SetUp() + call vimspector#test#setup#SetUpWithMappings( v:none ) +endfunction + +function! ClearDown() + call vimspector#test#setup#ClearDown() +endfunction + +function! SetUp_Test_Mappings_Are_Added_HUMAN() + let g:vimspector_enable_mappings = 'HUMAN' +endfunction + +function! Test_Mappings_Are_Added_HUMAN() + call assert_true( hasmapto( 'vimspector#Continue()' ) ) + call assert_false( hasmapto( 'vimspector#Launch()' ) ) + call assert_true( hasmapto( 'vimspector#Stop()' ) ) + call assert_true( hasmapto( 'vimspector#Restart()' ) ) + call assert_true( hasmapto( 'vimspector#ToggleBreakpoint()' ) ) + call assert_true( hasmapto( 'vimspector#AddFunctionBreakpoint' ) ) + call assert_true( hasmapto( 'vimspector#StepOver()' ) ) + call assert_true( hasmapto( 'vimspector#StepInto()' ) ) + call assert_true( hasmapto( 'vimspector#StepOut()' ) ) + call assert_true( hasmapto( 'vimspector#RunToCursor()' ) ) +endfunction + +function! SetUp_Test_Mappings_Are_Added_VISUAL_STUDIO() + let g:vimspector_enable_mappings = 'VISUAL_STUDIO' +endfunction + +function! Test_Mappings_Are_Added_VISUAL_STUDIO() + call assert_true( hasmapto( 'vimspector#Continue()' ) ) + call assert_false( hasmapto( 'vimspector#Launch()' ) ) + call assert_true( hasmapto( 'vimspector#Stop()' ) ) + call assert_true( hasmapto( 'vimspector#Restart()' ) ) + call assert_true( hasmapto( 'vimspector#ToggleBreakpoint()' ) ) + call assert_true( hasmapto( 'vimspector#AddFunctionBreakpoint' ) ) + call assert_true( hasmapto( 'vimspector#StepOver()' ) ) + call assert_true( hasmapto( 'vimspector#StepInto()' ) ) + call assert_true( hasmapto( 'vimspector#StepOut()' ) ) +endfunction + +function! SetUp_Test_Use_Mappings_HUMAN() + let g:vimspector_enable_mappings = 'HUMAN' +endfunction + +function! Test_Use_Mappings_HUMAN() + call ThisTestIsFlaky() + lcd testdata/cpp/simple + edit simple.cpp + call setpos( '.', [ 0, 15, 1 ] ) + + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( 'simple.cpp', 15, 1 ) + call vimspector#test#signs#AssertSignGroupEmptyAtLine( 'VimspectorBP', 15 ) + + " Add the breakpoint + call feedkeys( "\", 'xt' ) + call vimspector#test#signs#AssertSignGroupSingletonAtLine( 'VimspectorBP', + \ 15, + \ 'vimspectorBP', + \ 9 ) + + " Disable the breakpoint + call feedkeys( "\", 'xt' ) + call vimspector#test#signs#AssertSignGroupSingletonAtLine( + \ 'VimspectorBP', + \ 15, + \ 'vimspectorBPDisabled', + \ 9 ) + + " Delete the breakpoint + call feedkeys( "\", 'xt' ) + call vimspector#test#signs#AssertSignGroupEmptyAtLine( 'VimspectorBP', 15 ) + + " Add and clear using API + call vimspector#SetLineBreakpoint( 'simple.cpp', 15 ) + call vimspector#test#signs#AssertSignGroupSingletonAtLine( 'VimspectorBP', + \ 15, + \ 'vimspectorBP', + \ 9 ) + + call vimspector#ClearLineBreakpoint( 'simple.cpp', 15 ) + call vimspector#test#signs#AssertSignGroupEmptyAtLine( 'VimspectorBP', 15 ) + + " Add it again + call feedkeys( "\", 'xt' ) + call vimspector#test#signs#AssertSignGroupSingletonAtLine( + \ 'VimspectorBP', + \ 15, + \ 'vimspectorBP', + \ 9 ) + + " Here we go. Start Debugging + call feedkeys( "\", 'xt' ) + + call assert_equal( 2, len( gettabinfo() ) ) + let cur_tabnr = tabpagenr() + call assert_equal( 5, len( gettabinfo( cur_tabnr )[ 0 ].windows ) ) + + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( 'simple.cpp', 15, 1 ) + + " Step + call feedkeys( "\", 'xt' ) + + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( 'simple.cpp', 16, 1 ) + call WaitForAssert( {-> + \ vimspector#test#signs#AssertPCIsAtLineInBuffer( 'simple.cpp', 16 ) + \ } ) + + " Run to cursor (note , is the mapleader) + call cursor( 9, 1 ) + call feedkeys( ",\", 'xt' ) + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( 'simple.cpp', 9, 1 ) + call WaitForAssert( {-> + \ vimspector#test#signs#AssertPCIsAtLineInBuffer( 'simple.cpp', 9 ) + \ } ) + + " Stop + call feedkeys( "\", 'xt' ) + call WaitForAssert( {-> + \ assert_equal( [], + \ getbufline( g:vimspector_session_windows.variables, + \ 1, + \ '$' ) ) + \ } ) + call WaitForAssert( {-> + \ assert_equal( [], + \ getbufline( g:vimspector_session_windows.stack_trace, + \ 1, + \ '$' ) ) + \ } ) + call WaitForAssert( {-> + \ assert_equal( [], + \ getbufline( g:vimspector_session_windows.watches, + \ 1, + \ '$' ) ) + \ } ) + + call vimspector#test#setup#Reset() + + lcd - + %bwipeout! +endfunction + diff --git a/tests/python/Test_ExpandReferencesInDict.py b/tests/python/Test_ExpandReferencesInDict.py new file mode 100644 index 0000000..15fb11b --- /dev/null +++ b/tests/python/Test_ExpandReferencesInDict.py @@ -0,0 +1,89 @@ +import sys +import unittest +from unittest.mock import patch +from vimspector import utils + + +class TestExpandReferencesInDict( unittest.TestCase ): + def __init__( self, *args, **kwargs ): + super().__init__( *args, **kwargs ) + self.maxDiff = 4096 + + def test_ExpandReferencesInDict( self ): + mapping = { + 'one': 'one', + 'two': 'TWO', + 'bool': True, + 'words': 'these are some words' + } + calculus = { + 'three': lambda : 1 + 2, + } + CHOICES = { + 'five': '5ive!' + } + + def AskForInput( prompt, default_value = None ): + if default_value is not None: + return default_value + + return 'typed text' + + d = { + 'dollar': '$$', + 'not_a_var': '$${test}', + 'one': '${one}', + 'two': '${one} and ${two}', + 'three': '${three}', + 'three_with_default': '${three_with_default:${three\\}}', # uses calculus + 'four': '${four}', + 'five': '${five}', + 'list': [ '*${words}' ], + 'list1': [ 'start', '*${words}', 'end' ], + 'list2': [ '*${words}', '${three}' ], + 'list3': [ '${one}', '*${words}', 'three' ], + 'dict#json': '{ "key": "value" }', + 'bool#json': 'false', + 'one_default': '${one_default:one}', + 'two_default': '${two_default_1:one} and ${two_default_2:two}', + 'one_default2': '${one_default2:${one\\}}', + 'two_default2': + '${two_default2_1:${one\\}} and ${two_default2_2:${two\\}}', + 'unlikely_name#json#s': 'true', + 'empty_splice': [ '*${empty:}' ], + } + + e = { + 'dollar': '$', + 'not_a_var': '${test}', + 'one': 'one', + 'two': 'one and TWO', + 'three': '3', + 'three_with_default': '3', + 'four': 'typed text', + 'five': '5ive!', + 'list': [ 'these', 'are', 'some', 'words' ], + 'list1': [ 'start', 'these', 'are', 'some', 'words', 'end' ], + 'list2': [ 'these', 'are', 'some', 'words', '3' ], + 'list3': [ 'one', 'these', 'are', 'some', 'words', 'three' ], + 'dict': { + 'key': 'value', + }, + 'bool': False, + 'one_default': 'one', + 'two_default': 'one and two', + 'one_default2': 'one', + 'two_default2': 'one and TWO', + 'unlikely_name#json': 'true', + 'empty_splice': [], + } + + with patch( 'vimspector.utils.AskForInput', side_effect = AskForInput ): + utils.ExpandReferencesInDict( d, mapping, calculus, CHOICES ) + + self.assertDictEqual( d, e ) + + +assert unittest.main( module=__name__, + testRunner=unittest.TextTestRunner( sys.stdout ), + exit=False ).result.wasSuccessful() diff --git a/tests/stack_trace.test.vim b/tests/stack_trace.test.vim new file mode 100644 index 0000000..b5ed795 --- /dev/null +++ b/tests/stack_trace.test.vim @@ -0,0 +1,561 @@ +let s:fn='testdata/cpp/simple/threads.cpp' + +function! SetUp() + call vimspector#test#setup#SetUpWithMappings( 'HUMAN' ) +endfunction + +function! ClearDown() + call vimspector#test#setup#ClearDown() +endfunction + +function! s:StartDebugging() + exe 'edit ' . s:fn + call vimspector#SetLineBreakpoint( s:fn, 15 ) + call vimspector#LaunchWithSettings( #{ configuration: 'run-to-breakpoint' } ) + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( s:fn, 15, 1 ) +endfunction + +function! Test_Multiple_Threads_Continue() + let thread_l = 67 + let notify_l = 74 + + call vimspector#SetLineBreakpoint( s:fn, thread_l ) + call vimspector#SetLineBreakpoint( s:fn, notify_l ) + call s:StartDebugging() + + call vimspector#Continue() + + " As we step through the thread creation we should get Thread events + + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( s:fn, thread_l, 1 ) + call cursor( 1, 1 ) + call WaitForAssert( {-> + \ AssertMatchList( + \ [ + \ '- Thread [0-9]\+: .* (paused)', + \ ' .*: .*@threads.cpp:' . string( thread_l ) + \ ], + \ GetBufLine( winbufnr( g:vimspector_session_windows.stack_trace ), + \ 1, + \ 2 ) + \ ) + \ } ) + call vimspector#Continue() + + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( s:fn, thread_l, 1 ) + call cursor( 1, 1 ) + call WaitForAssert( {-> + \ AssertMatchList( + \ [ + \ '- Thread [0-9]\+: .* (paused)', + \ ' .*: .*@threads.cpp:' . string( thread_l ) + \ ], + \ GetBufLine( winbufnr( g:vimspector_session_windows.stack_trace ), + \ 1, + \ 2 ) + \ ) + \ } ) + call WaitForAssert( {-> + \ AssertMatchList( + \ [ + \ '+ Thread [0-9]\+: .* (paused)', + \ ], + \ GetBufLine( winbufnr( g:vimspector_session_windows.stack_trace ), + \ '$', + \ '$' ) + \ ) + \ } ) + call vimspector#Continue() + + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( s:fn, thread_l, 1 ) + call cursor( 1, 1 ) + call WaitForAssert( {-> + \ AssertMatchList( + \ [ + \ '- Thread [0-9]\+: .* (paused)', + \ ' .*: .*@threads.cpp:' . string( thread_l ) + \ ], + \ GetBufLine( winbufnr( g:vimspector_session_windows.stack_trace ), + \ 1, + \ 2 ) + \ ) + \ } ) + call WaitForAssert( {-> + \ AssertMatchList( + \ [ + \ '+ Thread [0-9]\+: .* (paused)', + \ ], + \ GetBufLine( winbufnr( g:vimspector_session_windows.stack_trace ), + \ '$', + \ '$' ) + \ ) + \ } ) + call vimspector#Continue() + + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( s:fn, thread_l, 1 ) + call cursor( 1, 1 ) + call WaitForAssert( {-> + \ AssertMatchList( + \ [ + \ '- Thread [0-9]\+: .* (paused)', + \ ' .*: .*@threads.cpp:' . string( thread_l ) + \ ], + \ GetBufLine( winbufnr( g:vimspector_session_windows.stack_trace ), + \ 1, + \ 2 ) + \ ) + \ } ) + call WaitForAssert( {-> + \ AssertMatchList( + \ [ + \ '+ Thread [0-9]\+: .* (paused)', + \ ], + \ GetBufLine( winbufnr( g:vimspector_session_windows.stack_trace ), + \ '$', + \ '$' ) + \ ) + \ } ) + call vimspector#Continue() + + " This is the last one + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( s:fn, thread_l, 1 ) + call cursor( 1, 1 ) + call WaitForAssert( {-> + \ AssertMatchList( + \ [ + \ '- Thread [0-9]\+: .* (paused)', + \ ' .*: .*@threads.cpp:' . string( thread_l ) + \ ], + \ GetBufLine( winbufnr( g:vimspector_session_windows.stack_trace ), + \ 1, + \ 2 ) + \ ) + \ } ) + call WaitForAssert( {-> + \ AssertMatchList( + \ [ + \ '+ Thread [0-9]\+: .* (paused)', + \ ], + \ GetBufLine( winbufnr( g:vimspector_session_windows.stack_trace ), + \ '$', + \ '$' ) + \ ) + \ } ) + call vimspector#Continue() + + " So we break out of the loop + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( s:fn, notify_l, 1 ) + call WaitForAssert( {-> + \ AssertMatchList( + \ [ + \ '- Thread [0-9]\+: .* (paused)', + \ ' .*: .*@threads.cpp:' . string( notify_l ) + \ ], + \ GetBufLine( winbufnr( g:vimspector_session_windows.stack_trace ), + \ 1, + \ 2 ) + \ ) + \ } ) + call WaitForAssert( {-> + \ AssertMatchList( + \ [ + \ '+ Thread [0-9]\+: .* (paused)', + \ ], + \ GetBufLine( winbufnr( g:vimspector_session_windows.stack_trace ), + \ '$', + \ '$' ) + \ ) + \ } ) + + call vimspector#ClearBreakpoints() + call vimspector#test#setup#Reset() + %bwipe! +endfunction + +function! Test_Multiple_Threads_Step() + let thread_l = 67 + if $VIMSPECTOR_MIMODE ==# 'lldb' + " } + let thread_n = thread_l + 1 + else + " for .... + let thread_n = 49 + endif + let notify_l = 74 + + call vimspector#SetLineBreakpoint( s:fn, thread_l ) + call vimspector#SetLineBreakpoint( s:fn, notify_l ) + call s:StartDebugging() + call vimspector#Continue() + + " As we step through the thread creation we should get Thread events + + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( s:fn, thread_l, 1 ) + call WaitForAssert( {-> + \ AssertMatchList( + \ [ + \ '- Thread [0-9]\+: .* (paused)', + \ ' .*: .*@threads.cpp:' . string( thread_l ) + \ ], + \ GetBufLine( winbufnr( g:vimspector_session_windows.stack_trace ), + \ 1, + \ 2 ) + \ ) + \ } ) + call vimspector#StepOver() + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( s:fn, thread_n, 1 ) + call WaitForAssert( {-> + \ AssertMatchList( + \ [ + \ '+ Thread [0-9]\+: .* (paused)', + \ ], + \ GetBufLine( winbufnr( g:vimspector_session_windows.stack_trace ), + \ '$', + \ '$' ) + \ ) + \ } ) + call vimspector#Continue() + + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( s:fn, thread_l, 1 ) + call WaitForAssert( {-> + \ AssertMatchList( + \ [ + \ '+ Thread [0-9]\+: .* (paused)', + \ ], + \ GetBufLine( winbufnr( g:vimspector_session_windows.stack_trace ), + \ '$', + \ '$' ) + \ ) + \ } ) + call vimspector#StepOver() + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( s:fn, thread_n, 1 ) + call WaitForAssert( {-> + \ AssertMatchList( + \ [ + \ '+ Thread [0-9]\+: .* (paused)', + \ '+ Thread [0-9]\+: .* (paused)', + \ ], + \ GetBufLine( winbufnr( g:vimspector_session_windows.stack_trace ), + \ -1, + \ '$' ) + \ ) + \ } ) + call vimspector#Continue() + + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( s:fn, thread_l, 1 ) + call WaitForAssert( {-> + \ AssertMatchList( + \ [ + \ '+ Thread [0-9]\+: .* (paused)', + \ '+ Thread [0-9]\+: .* (paused)', + \ ], + \ GetBufLine( winbufnr( g:vimspector_session_windows.stack_trace ), + \ -1, + \ '$' ) + \ ) + \ } ) + call vimspector#StepOver() + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( s:fn, thread_n, 1 ) + call WaitForAssert( {-> + \ AssertMatchList( + \ [ + \ '+ Thread [0-9]\+: .* (paused)', + \ '+ Thread [0-9]\+: .* (paused)', + \ '+ Thread [0-9]\+: .* (paused)', + \ ], + \ GetBufLine( winbufnr( g:vimspector_session_windows.stack_trace ), + \ -2, + \ '$' ) + \ ) + \ } ) + call vimspector#Continue() + + + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( s:fn, thread_l, 1 ) + call WaitForAssert( {-> + \ AssertMatchList( + \ [ + \ '+ Thread [0-9]\+: .* (paused)', + \ '+ Thread [0-9]\+: .* (paused)', + \ '+ Thread [0-9]\+: .* (paused)', + \ ], + \ GetBufLine( winbufnr( g:vimspector_session_windows.stack_trace ), + \ -2, + \ '$' ) + \ ) + \ } ) + call vimspector#StepOver() + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( s:fn, thread_n, 1 ) + call WaitForAssert( {-> + \ AssertMatchList( + \ [ + \ '+ Thread [0-9]\+: .* (paused)', + \ '+ Thread [0-9]\+: .* (paused)', + \ '+ Thread [0-9]\+: .* (paused)', + \ '+ Thread [0-9]\+: .* (paused)', + \ ], + \ GetBufLine( winbufnr( g:vimspector_session_windows.stack_trace ), + \ -3, + \ '$' ) + \ ) + \ } ) + call vimspector#Continue() + + + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( s:fn, thread_l, 1 ) + call WaitForAssert( {-> + \ AssertMatchList( + \ [ + \ '+ Thread [0-9]\+: .* (paused)', + \ '+ Thread [0-9]\+: .* (paused)', + \ '+ Thread [0-9]\+: .* (paused)', + \ '+ Thread [0-9]\+: .* (paused)', + \ ], + \ GetBufLine( winbufnr( g:vimspector_session_windows.stack_trace ), + \ -3, + \ '$' ) + \ ) + \ } ) + call vimspector#StepOver() + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( s:fn, thread_n, 1 ) + call WaitForAssert( {-> + \ AssertMatchList( + \ [ + \ '+ Thread [0-9]\+: .* (paused)', + \ '+ Thread [0-9]\+: .* (paused)', + \ '+ Thread [0-9]\+: .* (paused)', + \ '+ Thread [0-9]\+: .* (paused)', + \ '+ Thread [0-9]\+: .* (paused)', + \ ], + \ GetBufLine( winbufnr( g:vimspector_session_windows.stack_trace ), + \ -4, + \ '$' ) + \ ) + \ } ) + call vimspector#Continue() + + + " So we break out of the loop + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( s:fn, notify_l, 1 ) + call WaitForAssert( {-> + \ AssertMatchList( + \ [ + \ '+ Thread [0-9]\+: .* (paused)', + \ '+ Thread [0-9]\+: .* (paused)', + \ '+ Thread [0-9]\+: .* (paused)', + \ '+ Thread [0-9]\+: .* (paused)', + \ '+ Thread [0-9]\+: .* (paused)', + \ ], + \ GetBufLine( winbufnr( g:vimspector_session_windows.stack_trace ), + \ -4, + \ '$' ) + \ ) + \ } ) + + call vimspector#ClearBreakpoints() + call vimspector#test#setup#Reset() + %bwipe! +endfunction + +function! Test_UpDownStack() + let fn='../support/test/python/simple_python/main.py' + exe 'edit ' . fn + call setpos( '.', [ 0, 6, 1 ] ) + + call vimspector#SetLineBreakpoint( fn, 15 ) + call vimspector#LaunchWithSettings( { 'configuration': 'run' } ) + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( fn, 15, 1 ) + call WaitForAssert( {-> + \ AssertMatchList( + \ [ + \ '- Thread 1: MainThread (paused)', + \ ' 2: DoSomething@main.py:15', + \ ' 3: __init__@main.py:8', + \ ' 4: Main@main.py:23', + \ ' 5: @main.py:29', + \ ], + \ GetBufLine( winbufnr( g:vimspector_session_windows.stack_trace ), + \ 1, + \ '$' ) + \ ) + \ } ) + call win_gotoid( g:vimspector_session_windows.stack_trace ) + call WaitForAssert( { -> + \ vimspector#test#signs#AssertSignGroupSingletonAtLine( + \ 'VimspectorStackTrace', + \ 1, + \ 'vimspectorCurrentThread', + \ 200 ) } ) + call WaitForAssert( { -> + \ vimspector#test#signs#AssertSignGroupSingletonAtLine( + \ 'VimspectorStackTrace', + \ 2, + \ 'vimspectorCurrentFrame', + \ 200 ) } ) + wincmd w + + call vimspector#DownFrame() + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( fn, 15, 1 ) + call WaitForAssert( {-> + \ AssertMatchList( + \ [ + \ '- Thread 1: MainThread (paused)', + \ ' 2: DoSomething@main.py:15', + \ ' 3: __init__@main.py:8', + \ ' 4: Main@main.py:23', + \ ' 5: @main.py:29', + \ ], + \ GetBufLine( winbufnr( g:vimspector_session_windows.stack_trace ), + \ 1, + \ '$' ) + \ ) + \ } ) + call win_gotoid( g:vimspector_session_windows.stack_trace ) + call WaitForAssert( { -> + \ vimspector#test#signs#AssertSignGroupSingletonAtLine( + \ 'VimspectorStackTrace', + \ 1, + \ 'vimspectorCurrentThread', + \ 200 ) } ) + call WaitForAssert( { -> + \ vimspector#test#signs#AssertSignGroupSingletonAtLine( + \ 'VimspectorStackTrace', + \ 2, + \ 'vimspectorCurrentFrame', + \ 200 ) } ) + wincmd w + + + call vimspector#UpFrame() + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( fn, 8, 1 ) + call WaitForAssert( {-> + \ AssertMatchList( + \ [ + \ '- Thread 1: MainThread (paused)', + \ ' 2: DoSomething@main.py:15', + \ ' 3: __init__@main.py:8', + \ ' 4: Main@main.py:23', + \ ' 5: @main.py:29', + \ ], + \ GetBufLine( winbufnr( g:vimspector_session_windows.stack_trace ), + \ 1, + \ '$' ) + \ ) + \ } ) + call win_gotoid( g:vimspector_session_windows.stack_trace ) + call WaitForAssert( { -> + \ vimspector#test#signs#AssertSignGroupSingletonAtLine( + \ 'VimspectorStackTrace', + \ 1, + \ 'vimspectorCurrentThread', + \ 200 ) } ) + call WaitForAssert( { -> + \ vimspector#test#signs#AssertSignGroupSingletonAtLine( + \ 'VimspectorStackTrace', + \ 3, + \ 'vimspectorCurrentFrame', + \ 200 ) } ) + wincmd w + + + call feedkeys( "\VimspectorUpFrame", 'x' ) + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( fn, 23, 1 ) + call WaitForAssert( {-> + \ AssertMatchList( + \ [ + \ '- Thread 1: MainThread (paused)', + \ ' 2: DoSomething@main.py:15', + \ ' 3: __init__@main.py:8', + \ ' 4: Main@main.py:23', + \ ' 5: @main.py:29', + \ ], + \ GetBufLine( winbufnr( g:vimspector_session_windows.stack_trace ), + \ 1, + \ '$' ) + \ ) + \ } ) + call win_gotoid( g:vimspector_session_windows.stack_trace ) + call WaitForAssert( { -> + \ vimspector#test#signs#AssertSignGroupSingletonAtLine( + \ 'VimspectorStackTrace', + \ 1, + \ 'vimspectorCurrentThread', + \ 200 ) } ) + call WaitForAssert( { -> + \ vimspector#test#signs#AssertSignGroupSingletonAtLine( + \ 'VimspectorStackTrace', + \ 4, + \ 'vimspectorCurrentFrame', + \ 200 ) } ) + wincmd w + + + call feedkeys( "\VimspectorDownFrame", 'x' ) + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( fn, 8, 1 ) + call WaitForAssert( {-> + \ AssertMatchList( + \ [ + \ '- Thread 1: MainThread (paused)', + \ ' 2: DoSomething@main.py:15', + \ ' 3: __init__@main.py:8', + \ ' 4: Main@main.py:23', + \ ' 5: @main.py:29', + \ ], + \ GetBufLine( winbufnr( g:vimspector_session_windows.stack_trace ), + \ 1, + \ '$' ) + \ ) + \ } ) + call win_gotoid( g:vimspector_session_windows.stack_trace ) + call WaitForAssert( { -> + \ vimspector#test#signs#AssertSignGroupSingletonAtLine( + \ 'VimspectorStackTrace', + \ 1, + \ 'vimspectorCurrentThread', + \ 200 ) } ) + call WaitForAssert( { -> + \ vimspector#test#signs#AssertSignGroupSingletonAtLine( + \ 'VimspectorStackTrace', + \ 3, + \ 'vimspectorCurrentFrame', + \ 200 ) } ) + wincmd w + + + call vimspector#DownFrame() + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( fn, 15, 1 ) + call WaitForAssert( {-> + \ AssertMatchList( + \ [ + \ '- Thread 1: MainThread (paused)', + \ ' 2: DoSomething@main.py:15', + \ ' 3: __init__@main.py:8', + \ ' 4: Main@main.py:23', + \ ' 5: @main.py:29', + \ ], + \ GetBufLine( winbufnr( g:vimspector_session_windows.stack_trace ), + \ 1, + \ '$' ) + \ ) + \ } ) + call win_gotoid( g:vimspector_session_windows.stack_trace ) + call WaitForAssert( { -> + \ vimspector#test#signs#AssertSignGroupSingletonAtLine( + \ 'VimspectorStackTrace', + \ 1, + \ 'vimspectorCurrentThread', + \ 200 ) } ) + call WaitForAssert( { -> + \ vimspector#test#signs#AssertSignGroupSingletonAtLine( + \ 'VimspectorStackTrace', + \ 2, + \ 'vimspectorCurrentFrame', + \ 200 ) } ) + wincmd w + + + + call vimspector#ClearBreakpoints() + call vimspector#test#setup#Reset() + %bwipe! +endfunction diff --git a/tests/tabpage.test.vim b/tests/tabpage.test.vim index 0aefe7f..92b57e8 100644 --- a/tests/tabpage.test.vim +++ b/tests/tabpage.test.vim @@ -10,7 +10,7 @@ function! Test_Step_With_Different_Tabpage() lcd testdata/cpp/simple edit simple.cpp - " Add the breakpoing + " Add the breakpoint " TODO refactor FeedKeys 15 call assert_equal( 15, line( '.' ) ) @@ -28,7 +28,7 @@ function! Test_Step_With_Different_Tabpage() call assert_equal( 1, col( '.' ), 'Current column' ) " Switch to the other tab - normal gt + normal! gt call assert_notequal( vimspector_tabnr, tabpagenr() ) @@ -44,10 +44,113 @@ function! Test_Step_With_Different_Tabpage() call assert_equal( 'simple.cpp', bufname( '%' ), 'Current buffer' ) call assert_equal( 1, col( '.' ), 'Current column' ) - call vimspector#Reset() - call vimspector#ClearBreakpoints() - + call vimspector#test#setup#Reset() lcd - %bwipeout! endfunction +function! Test_All_Buffers_Deleted_NoHidden() + call ThisTestIsFlaky() + + set nohidden + lcd testdata/cpp/simple + edit simple.cpp + + let opts = #{ buflisted: v:true } + + let buffers_before = getbufinfo( opts ) + + call setpos( '.', [ 0, 15, 1 ] ) + call vimspector#ToggleBreakpoint() + call vimspector#Launch() + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( 'simple.cpp', 15, 1 ) + call vimspector#test#signs#AssertPCIsAtLineInBuffer( + \ 'simple.cpp', + \ 15 ) + + call vimspector#Reset() + call vimspector#test#setup#WaitForReset() + + call WaitForAssert( {-> + \ assert_equal( len( buffers_before ), len( getbufinfo( opts ) ) ) } ) + + set hidden& + lcd - + call vimspector#test#setup#Reset() + %bwipe! +endfunction + +function! Test_All_Buffers_Deleted_Hidden() + call ThisTestIsFlaky() + + set hidden + lcd testdata/cpp/simple + edit simple.cpp + + let opts = #{ buflisted: v:true } + + let buffers_before = getbufinfo( opts ) + + call setpos( '.', [ 0, 15, 1 ] ) + call vimspector#ToggleBreakpoint() + call vimspector#Launch() + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( 'simple.cpp', 15, 1 ) + call vimspector#test#signs#AssertPCIsAtLineInBuffer( + \ 'simple.cpp', + \ 15 ) + + call vimspector#Reset() + call vimspector#test#setup#WaitForReset() + + call WaitForAssert( {-> + \ assert_equal( len( buffers_before ), len( getbufinfo( opts ) ) ) } ) + + set hidden& + lcd - + call vimspector#test#setup#Reset() + %bwipe! +endfunction + +function! Test_All_Buffers_Deleted_ToggleLog() + set hidden + let buffers_before = getbufinfo( #{ buflisted: 1 } ) + VimspectorToggleLog + VimspectorToggleLog + + call WaitForAssert( {-> + \ assert_equal( len( buffers_before ), + \ len( getbufinfo( #{ buflisted: 1 } ) ) ) } ) + + call vimspector#test#setup#Reset() + set hidden& + %bwipe! +endfunction + +let g:vimspector_test_install_done = 0 + +function! Test_All_Buffers_Deleted_Installer() + set hidden + let buffers_before = getbufinfo( #{ buflisted: 1 } ) + + augroup Test_All_Buffers_Deleted_Installer + au! + au User VimspectorInstallSuccess let g:vimspector_test_install_done = 1 + au User VimspectorInstallFailed let g:vimspector_test_install_done = 1 + augroup END + + VimspectorUpdate + + " The test timeout will take care of this taking too long + call WaitForAssert( + \ { -> assert_equal( 1, g:vimspector_test_install_done ) }, + \ 120000 ) + + call WaitForAssert( {-> + \ assert_equal( len( buffers_before ), + \ len( getbufinfo( #{ buflisted: 1 } ) ) ) } ) + + call vimspector#test#setup#Reset() + set hidden& + au! Test_All_Buffers_Deleted_Installer + %bwipe! +endfunction diff --git a/tests/temporary_breakpoints.test.vim b/tests/temporary_breakpoints.test.vim new file mode 100644 index 0000000..7bcad55 --- /dev/null +++ b/tests/temporary_breakpoints.test.vim @@ -0,0 +1,200 @@ +function! SetUp() + call vimspector#test#setup#SetUpWithMappings( v:none ) +endfunction + +function! ClearDown() + call vimspector#test#setup#ClearDown() +endfunction + +function s:Start() + call vimspector#SetLineBreakpoint( 'moo.py', 13 ) + call vimspector#Launch() + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( 'moo.py', 1, 1 ) + call vimspector#test#signs#AssertPCIsAtLineInBuffer( 'moo.py', 1 ) + call vimspector#Continue() + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( 'moo.py', 13, 1 ) + call vimspector#test#signs#AssertPCIsAtLineInBuffer( 'moo.py', 13 ) +endfunction + +function Test_Run_To_Cursor_Simple() + " Run to a position that will certainly be executed + lcd ../support/test/python/multiple_files + call vimspector#SetLineBreakpoint( 'moo.py', 13 ) + call s:Start() + + call cursor( 8, 27 ) + call vimspector#RunToCursor() + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( 'moo.py', 8, 27 ) + call WaitForAssert( {-> + \ vimspector#test#signs#AssertPCIsAtLineInBuffer( 'moo.py', 8 ) + \ } ) + call vimspector#StepOver() + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( 'moo.py', 9, 1 ) + call vimspector#test#signs#AssertPCIsAtLineInBuffer( 'moo.py', 9 ) + " Check there is no breakpoint set on line 8 + call WaitForAssert( {-> + \ vimspector#test#signs#AssertSignGroupEmptyAtLine( 'VimspectorCode', 8 ) + \ } ) + call vimspector#test#setup#Reset() + lcd - + %bwipe! +endfunction + +function Test_Run_To_Cursor_On_NonBreaking_Line() + " Run to a position that will certainly be executed, but is not a real line + lcd ../support/test/python/multiple_files + call vimspector#SetLineBreakpoint( 'moo.py', 13 ) + call s:Start() + + call cursor( 7, 1 ) + " Interestingly, debugpy moves the breakpoint to the previous line, which is + " kinda annoying + call vimspector#RunToCursor() + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( 'moo.py', 6, 1 ) + call WaitForAssert( {-> + \ vimspector#test#signs#AssertPCIsAtLineInBuffer( 'moo.py', 6 ) + \ } ) + call vimspector#StepOver() + " It's a loop, so we go up a line + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( 'moo.py', 5, 1 ) + call vimspector#test#signs#AssertPCIsAtLineInBuffer( 'moo.py', 5 ) + + " Check there is no breakpoint set on lines 7 and 6: + " 7 - where we put the 'temporary' breakpoint + " 6 - where it got placed + " + " FIXME: This is broken, we don't _know_ that the breakpoint that was hit was + " the temporary one, and there's no way to know. + " + " I wonder if the relocated breakpoint can be matched with the _original_ + " breakpoint + call WaitForAssert( {-> + \ vimspector#test#signs#AssertSignGroupEmptyAtLine( 'VimspectorCode', 7 ) + \ } ) + call WaitForAssert( {-> + \ vimspector#test#signs#AssertSignGroupEmptyAtLine( 'VimspectorCode', 6 ) + \ } ) + call vimspector#test#setup#Reset() + lcd - + %bwipe! +endfunction + +function Test_Run_To_Cursor_Different_File() + " Run into a different file + " Run to a position that will certainly be executed, but is not a real line + lcd ../support/test/python/multiple_files + call vimspector#SetLineBreakpoint( 'moo.py', 13 ) + call s:Start() + + edit cow.py + call cursor( 2, 1 ) + call vimspector#RunToCursor() + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( 'cow.py', 2, 1 ) + call WaitForAssert( {-> + \ vimspector#test#signs#AssertPCIsAtLineInBuffer( 'cow.py', 2 ) + \ } ) + + bu moo.py + call cursor( 9, 12 ) + call vimspector#RunToCursor() + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( 'moo.py', 9, 12 ) + call WaitForAssert( {-> + \ vimspector#test#signs#AssertPCIsAtLineInBuffer( 'moo.py', 9 ) + \ } ) + + call vimspector#test#setup#Reset() + lcd - + %bwipe! +endfunction + +function Test_Run_To_Cursor_Hit_Another_Breakpoint() + " Run to cursor, but hit a non-temporary breakpoint + lcd ../support/test/python/multiple_files + call vimspector#SetLineBreakpoint( 'moo.py', 13 ) + call s:Start() + + call vimspector#SetLineBreakpoint( 'moo.py', 5 ) + call cursor( 6, 1 ) + + call vimspector#RunToCursor() + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( 'moo.py', 5, 1 ) + call WaitForAssert( {-> + \ vimspector#test#signs#AssertPCIsAtLineInBuffer( 'moo.py', 5 ) + \ } ) + + " The temporary breakpoint is still there + call vimspector#test#signs#AssertSignGroupSingletonAtLine( + \ 'VimspectorCode', + \ 6, + \ 'vimspectorBP', + \ 9 ) + + call vimspector#ClearLineBreakpoint( 'moo.py', 5 ) + + call cursor( 8, 1 ) + call vimspector#RunToCursor() + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( 'moo.py', 8, 1 ) + call WaitForAssert( {-> + \ vimspector#test#signs#AssertPCIsAtLineInBuffer( 'moo.py', 8 ) + \ } ) + call WaitForAssert( {-> + \ vimspector#test#signs#AssertSignGroupEmptyAtLine( 'VimspectorCode', 6 ) + \ } ) + + call vimspector#test#setup#Reset() + lcd - + %bwipe! +endfunction + +function! Test_InvalidBreakpoint() + " Run to cursor, but hit a non-temporary breakpoint + lcd ../support/test/python/multiple_files + call vimspector#SetLineBreakpoint( 'moo.py', 13 ) + call s:Start() + + call vimspector#SetLineBreakpoint( 'moo.py', 9 ) + + edit .vimspector.json + call cursor( 1, 1 ) + call vimspector#RunToCursor() + + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( 'moo.py', 9, 1 ) + call WaitForAssert( {-> + \ vimspector#test#signs#AssertPCIsAtLineInBuffer( 'moo.py', 9 ) + \ } ) + + call vimspector#test#setup#Reset() + lcd - + %bwipe! +endfunction + +function! Test_StartDebuggingWithRunToCursor() + lcd ../support/test/python/multiple_files + edit moo.py + call cursor( 9, 1 ) + call vimspector#RunToCursor() + " Stop on entry is still hit + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( 'moo.py', 1, 1 ) + call vimspector#test#signs#AssertPCIsAtLineInBuffer( 'moo.py', 1 ) + call vimspector#test#signs#AssertSignGroupSingletonAtLine( + \ 'VimspectorCode', + \ 9, + \ 'vimspectorBP', + \ 9 ) + + call vimspector#Continue() + " Runs to cursor + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( 'moo.py', 9, 1 ) + call vimspector#test#signs#AssertPCIsAtLineInBuffer( 'moo.py', 9 ) + + call vimspector#StepOver() + " And claers the temp breakpoint + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( 'moo.py', 8, 1 ) + call vimspector#test#signs#AssertPCIsAtLineInBuffer( 'moo.py', 8 ) + + call WaitForAssert( {-> + \ vimspector#test#signs#AssertSignGroupEmptyAtLine( 'VimspectorCode', 9 ) + \ } ) + + lcd - +endfunction diff --git a/tests/testdata/cpp/simple/.gitignore b/tests/testdata/cpp/simple/.gitignore index ac54bdc..d64075c 100644 --- a/tests/testdata/cpp/simple/.gitignore +++ b/tests/testdata/cpp/simple/.gitignore @@ -1,2 +1,5 @@ simple variables +struct +printer +threads diff --git a/tests/testdata/cpp/simple/.vimspector.json b/tests/testdata/cpp/simple/.vimspector.json index 9fa71d2..48ce801 100644 --- a/tests/testdata/cpp/simple/.vimspector.json +++ b/tests/testdata/cpp/simple/.vimspector.json @@ -1,18 +1,134 @@ { + "$schema": "https://puremourning.github.io/vimspector/schema/vimspector.schema.json", "configurations": { - "cpptools-run": { + "run-to-entry": { "adapter": "vscode-cpptools", + // This makes this configuration the default. Only one default can be set + // (having two is the same as having none) + "default": true, "configuration": { - "type": "cppdbg", "request": "launch", "program": "${workspaceRoot}/${fileBasenameNoExtension}", - "args": [], - "cwd": "${workspaceRoot}", - "environment": [], - "externalConsole": true, + "externalConsole": false, "stopAtEntry": true, - "MImode": "${VIMSPECTOR_MIMODE}" + "stopOnEntry": true, + "MIMode": "${VIMSPECTOR_MIMODE}" + }, + "breakpoints": { + "exception": { + "cpp_catch": "", + "cpp_throw": "", + "objc_catch": "", + "objc_throw": "", + "swift_catch": "", + "swift_throw": "" + } } + }, + "run-to-breakpoint": { + "adapter": "vscode-cpptools", + "configuration": { + "request": "launch", + "program": "${workspaceRoot}/${fileBasenameNoExtension}", + "externalConsole": false, + "stopAtEntry": false, + "stopOnEntry": false, + "MIMode": "${VIMSPECTOR_MIMODE}" + }, + "breakpoints": { + "exception": { + "cpp_catch": "", + "cpp_throw": "", + "objc_catch": "", + "objc_throw": "", + "swift_catch": "", + "swift_throw": "" + } + } + }, + "run-to-breakpoint-specify-file": { + "adapter": "vscode-cpptools", + "configuration": { + "request": "launch", + "program": "${workspaceRoot}/${prog}", + "cwd": "${workspaceRoot}", + "externalConsole": false, + "stopAtEntry": false, + "stopOnEntry": false, + "MIMode": "${VIMSPECTOR_MIMODE}" + }, + "breakpoints": { + "exception": { + "cpp_catch": "", + "cpp_throw": "", + "objc_catch": "", + "objc_throw": "", + "swift_catch": "", + "swift_throw": "" + } + } + }, + "calculate-some-variable": { + "adapter": "vscode-cpptools", + "variables": { + "SIMPLE": "This is some text containing: $HOME", + "CALCULATED_LIST": { + "shell": [ "uuidgen" ] + }, + "CALCULATED_STR": { + "shell": [ "uuidgen" ] + } + }, + "configuration": { + "request": "launch", + "program": "${workspaceRoot}/${fileBasenameNoExtension}", + "MIMode": "${VIMSPECTOR_MIMODE}", + "externalConsole": false, + "args": [ + "CALCULATED_LIST", "${CALCULATED_LIST}", + "SIMPLE", "${SIMPLE}", + "CALCULATED_STR", "${CALCULATED_STR}" + ] + } + }, + "lldb-vscode": { + "adapter": "lldb-vscode", + "configuration": { + "request": "launch", + "program": "${workspaceRoot}/${fileBasenameNoExtension}", + "cwd": "${workspaceRoot}", + "externalConsole": false, + "MIMode": "lldb" + } + }, + "CodeLLDB": { + "adapter": "CodeLLDB", + "configuration": { + "request": "launch", + "program": "${workspaceRoot}/${fileBasenameNoExtension}", + "cwd": "${workspaceRoot}", + "expressions": "native" + } + } + }, + "adapters": { + "lldb-vscode": { + "variables": { + "LLVM": { + "shell": "brew --prefix llvm" + } + }, + "attach": { + "pidProperty": "pid", + "pidSelect": "ask" + }, + "command": [ + "${LLVM}/bin/lldb-vscode" + ], + "env": { + "LLDB_LAUNCH_FLAG_LAUNCH_IN_TTY": "YES" + }, + "name": "lldb" } } } diff --git a/tests/testdata/cpp/simple/.ycm_extra_conf.py b/tests/testdata/cpp/simple/.ycm_extra_conf.py index 78a71f0..c9117c6 100644 --- a/tests/testdata/cpp/simple/.ycm_extra_conf.py +++ b/tests/testdata/cpp/simple/.ycm_extra_conf.py @@ -2,6 +2,7 @@ def Settings( **kwargs ): return { 'flags': [ '-x', 'c++', + '-std=c++17', '-Wextra', '-Werror', '-Wall' ] } diff --git a/tests/testdata/cpp/simple/Makefile b/tests/testdata/cpp/simple/Makefile index 8583202..471edae 100644 --- a/tests/testdata/cpp/simple/Makefile +++ b/tests/testdata/cpp/simple/Makefile @@ -2,7 +2,8 @@ CXXFLAGS=-g -O0 -std=c++17 .PHONY: all -TARGETS=simple variables +TARGETS=simple variables struct printer threads +LDLIBS=-lpthread all: $(TARGETS) diff --git a/tests/testdata/cpp/simple/printer.cpp b/tests/testdata/cpp/simple/printer.cpp new file mode 100644 index 0000000..1e4a0b7 --- /dev/null +++ b/tests/testdata/cpp/simple/printer.cpp @@ -0,0 +1,21 @@ +#include +#include + +int main( int argc, char** argv ) +{ + if ( ( argc - 1 ) % 2 ) + { + std::cerr << "Unbalanced arguments\n"; + return 1; + } + + for ( int i=1; i < argc; i += 2 ) + { + std::cout << argv[ i ] + << ": " + << argv[ i + 1 ] + << '\n'; + } + + return 0; +} diff --git a/tests/testdata/cpp/simple/struct.cpp b/tests/testdata/cpp/simple/struct.cpp new file mode 100644 index 0000000..ea55efc --- /dev/null +++ b/tests/testdata/cpp/simple/struct.cpp @@ -0,0 +1,33 @@ +struct AnotherTest +{ + char choo; + int ints[5]; +}; + +struct Test +{ + int i; + char c; + float fffff; + + AnotherTest another_test; +}; + +static Test SetUp( Test t ) +{ + t.another_test.choo = 'p'; + t.another_test.ints[ 0 ] = t.i; return t; +} + +int main( int , char ** ) +{ + Test t = {}; + + t.i = 1; + t.c = 'c'; + t.fffff = 3.14; + + t = SetUp( t ); + + return t.another_test.ints[ 0 ]; +} diff --git a/tests/testdata/cpp/simple/threads.cpp b/tests/testdata/cpp/simple/threads.cpp new file mode 100644 index 0000000..445c09e --- /dev/null +++ b/tests/testdata/cpp/simple/threads.cpp @@ -0,0 +1,82 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +int main( int argc, char ** argv ) +{ + int numThreads = {}; + if ( argc < 2 ) + { + numThreads = 5; + } + else + { + std::string_view numThreadArg( argv[ 1 ] ); + if ( auto [ p, ec ] = std::from_chars( numThreadArg.begin(), + numThreadArg.end(), + numThreads ); + ec != std::errc() ) + { + std::cerr << "Usage " << argv[ 0 ] << " \n"; + return 2; + } + } + + std::cout << "Creating " << numThreads << " threads" << '\n'; + + std::vector threads{}; + threads.reserve( numThreads ); + + auto eng = std::default_random_engine() ; + auto dist = std::uniform_int_distribution( 250, 1000 ); + + std::mutex m; + std::condition_variable v; + bool ready = false; + { + std::lock_guard l(m); + + std::cout << "Preparing..." << '\n'; + + for ( int i = 0; i < numThreads; ++i ) + { + using namespace std::chrono_literals; + auto tp = [&,tnum=i]() { + // Wait for the go-ahead + { + std::unique_lock l(m); + while (!ready) { + v.wait(l); + } + } + + std::cout << "Started thread " << tnum << '\n'; + std::this_thread::sleep_for( + 5s + std::chrono::milliseconds( dist( eng ) ) ); + std::cout << "Completed thread " << tnum << '\n'; + }; + + threads.emplace_back( tp ); + } + + std::cout << "Ready to go!" << '\n'; + ready = true; + } + + v.notify_all(); + + for ( int i = 0; i < numThreads; ++i ) + { + threads[ i ].join(); + } + + return 0; +} diff --git a/tests/ui.test.vim b/tests/ui.test.vim new file mode 100644 index 0000000..5ba2f9c --- /dev/null +++ b/tests/ui.test.vim @@ -0,0 +1,711 @@ +let s:fn='../support/test/python/simple_python/main.py' + +function! SetUp() + let g:vimspector_ui_mode = get( s:, 'vimspector_ui_mode', 'horizontal' ) + call vimspector#test#setup#SetUpWithMappings( 'HUMAN' ) +endfunction + +function! ClearDown() + call vimspector#test#setup#ClearDown() +endfunction + +function! s:StartDebugging() + exe 'edit ' . s:fn + call setpos( '.', [ 0, 23, 1 ] ) + call vimspector#ToggleBreakpoint() + call vimspector#LaunchWithSettings( { 'configuration': 'run' } ) + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( s:fn, 23, 1 ) +endfunction + +function! SetUp_Test_StandardLayout() + call vimspector#test#setup#PushOption( 'columns', 200 ) +endfunction + +function! Test_StandardLayout() + call s:StartDebugging() + + call vimspector#StepOver() + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( s:fn, 25, 1 ) + + call assert_equal( 'horizontal', g:vimspector_session_windows.mode ) + call assert_equal( + \ [ 'row', [ + \ [ 'col', [ + \ [ 'leaf', g:vimspector_session_windows.variables ], + \ [ 'leaf', g:vimspector_session_windows.watches ], + \ [ 'leaf', g:vimspector_session_windows.stack_trace ], + \ ] ], + \ [ 'col', [ + \ [ 'row', [ + \ [ 'leaf', g:vimspector_session_windows.code ], + \ [ 'leaf', g:vimspector_session_windows.terminal ], + \ ] ], + \ [ 'leaf', g:vimspector_session_windows.output ], + \ ] ] + \ ] ], + \ winlayout( g:vimspector_session_windows.tabpage ) ) + + call vimspector#test#setup#Reset() + %bwipe! +endfunction + +function! TearDown_Test_StandardLayout() + call vimspector#test#setup#PopOption( 'columns' ) +endfunction + +function! SetUp_Test_NarrowLayout() + call vimspector#test#setup#PushOption( 'columns', 100 ) + let s:vimspector_ui_mode = 'vertical' +endfunction + +function! Test_NarrowLayout() + call s:StartDebugging() + + call vimspector#StepOver() + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( s:fn, 25, 1 ) + + call assert_equal( 'vertical', g:vimspector_session_windows.mode ) + call assert_equal( + \ [ 'col', [ + \ [ 'row', [ + \ [ 'leaf', g:vimspector_session_windows.variables ], + \ [ 'leaf', g:vimspector_session_windows.watches ], + \ [ 'leaf', g:vimspector_session_windows.stack_trace ], + \ ] ], + \ [ 'leaf', g:vimspector_session_windows.code ], + \ [ 'leaf', g:vimspector_session_windows.terminal ], + \ [ 'leaf', g:vimspector_session_windows.output ], + \ ] ], + \ winlayout( g:vimspector_session_windows.tabpage ) ) + + call vimspector#test#setup#Reset() + %bwipe! +endfunction + +function! TearDown_Test_NarrowLayout() + unlet s:vimspector_ui_mode + call vimspector#test#setup#PopOption( 'columns' ) +endfunction + +function! SetUp_Test_AutoLayoutTerminalVert() + let s:vimspector_ui_mode = 'auto' + call vimspector#test#setup#PushOption( 'columns', 250 ) + call vimspector#test#setup#PushOption( 'lines', 30 ) +endfunction + +function! Test_AutoLayoutTerminalVert() + call s:StartDebugging() + + call vimspector#StepOver() + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( s:fn, 25, 1 ) + + call assert_equal( 'horizontal', g:vimspector_session_windows.mode ) + call assert_equal( + \ [ 'row', [ + \ [ 'col', [ + \ [ 'leaf', g:vimspector_session_windows.variables ], + \ [ 'leaf', g:vimspector_session_windows.watches ], + \ [ 'leaf', g:vimspector_session_windows.stack_trace ], + \ ] ], + \ [ 'col', [ + \ [ 'row', [ + \ [ 'leaf', g:vimspector_session_windows.code ], + \ [ 'leaf', g:vimspector_session_windows.terminal ], + \ ] ], + \ [ 'leaf', g:vimspector_session_windows.output ], + \ ] ] + \ ] ], + \ winlayout( g:vimspector_session_windows.tabpage ) ) + + call vimspector#test#setup#Reset() + %bwipe! +endfunction + +function! TearDown_Test_AutoLayoutTerminalVert() + unlet s:vimspector_ui_mode + call vimspector#test#setup#PopOption( 'lines' ) + call vimspector#test#setup#PopOption( 'columns' ) +endfunction + +function! SetUp_Test_AutoLayoutTerminalHorizVert() + let s:vimspector_ui_mode = 'auto' + " Wide enough to be horizontal layout, but not wide enough to fully fit the + " terminal, with enough rows to fit the max terminal below + call vimspector#test#setup#PushOption( 'columns', + \ 50 + 82 + 3 + 2 + 12 ) + call vimspector#test#setup#PushOption( 'lines', 50 ) +endfunction + +function! Test_AutoLayoutTerminalHorizVert() + call s:StartDebugging() + + call vimspector#StepOver() + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( s:fn, 25, 1 ) + + call assert_equal( 'horizontal', g:vimspector_session_windows.mode ) + call assert_equal( + \ [ 'row', [ + \ [ 'col', [ + \ [ 'leaf', g:vimspector_session_windows.variables ], + \ [ 'leaf', g:vimspector_session_windows.watches ], + \ [ 'leaf', g:vimspector_session_windows.stack_trace ], + \ ] ], + \ [ 'col', [ + \ [ 'leaf', g:vimspector_session_windows.code ], + \ [ 'leaf', g:vimspector_session_windows.terminal ], + \ [ 'leaf', g:vimspector_session_windows.output ], + \ ] ] + \ ] ], + \ winlayout( g:vimspector_session_windows.tabpage ) ) + + call vimspector#test#setup#Reset() + %bwipe! +endfunction + +function! TearDown_Test_AutoLayoutTerminalHorizVert() + unlet s:vimspector_ui_mode + call vimspector#test#setup#PopOption( 'lines' ) + call vimspector#test#setup#PopOption( 'columns' ) +endfunction + +function! SetUp_Test_AutoLayoutTerminalHorizVertButNotEnoughLines() + let s:vimspector_ui_mode = 'auto' + " Wide enough to be horizontal layout, but not wide enough to fully fit the + " terminal, with enough rows to fit the max terminal below, but there are not + " enough lines to do this + call vimspector#test#setup#PushOption( 'columns', + \ 50 + 82 + 3 + 2 + 12 ) + call vimspector#test#setup#PushOption( 'lines', 20 ) +endfunction + +function! Test_AutoLayoutTerminalHorizVertButNotEnoughLines() + call s:StartDebugging() + + call vimspector#StepOver() + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( s:fn, 25, 1 ) + + call assert_equal( 'horizontal', g:vimspector_session_windows.mode ) + call assert_equal( + \ [ 'row', [ + \ [ 'col', [ + \ [ 'leaf', g:vimspector_session_windows.variables ], + \ [ 'leaf', g:vimspector_session_windows.watches ], + \ [ 'leaf', g:vimspector_session_windows.stack_trace ], + \ ] ], + \ [ 'col', [ + \ [ 'row', [ + \ [ 'leaf', g:vimspector_session_windows.code ], + \ [ 'leaf', g:vimspector_session_windows.terminal ], + \ ] ], + \ [ 'leaf', g:vimspector_session_windows.output ], + \ ] ], + \ ] ], + \ winlayout( g:vimspector_session_windows.tabpage ) ) + + call vimspector#test#setup#Reset() + %bwipe! +endfunction + +function! TearDown_Test_AutoLayoutTerminalHorizVertButNotEnoughLines() + unlet s:vimspector_ui_mode + call vimspector#test#setup#PopOption( 'lines' ) + call vimspector#test#setup#PopOption( 'columns' ) +endfunction + +function! SetUp_Test_AutoLayoutTerminalHoriz() + let s:vimspector_ui_mode = 'vertical' + " Vertical layout, but we split the terminal horizonally + call vimspector#test#setup#PushOption( 'columns', 200 ) + call vimspector#test#setup#PushOption( 'lines', 50 ) +endfunction + +function! Test_AutoLayoutTerminalHoriz() + call s:StartDebugging() + + call vimspector#StepOver() + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( s:fn, 25, 1 ) + + call assert_equal( 'vertical', g:vimspector_session_windows.mode ) + call assert_equal( + \ [ 'col', [ + \ [ 'row', [ + \ [ 'leaf', g:vimspector_session_windows.variables ], + \ [ 'leaf', g:vimspector_session_windows.watches ], + \ [ 'leaf', g:vimspector_session_windows.stack_trace ], + \ ] ], + \ [ 'row', [ + \ [ 'leaf', g:vimspector_session_windows.code ], + \ [ 'leaf', g:vimspector_session_windows.terminal ], + \ ] ], + \ [ 'leaf', g:vimspector_session_windows.output ], + \ ] ], + \ winlayout( g:vimspector_session_windows.tabpage ) ) + + call vimspector#test#setup#Reset() + %bwipe! +endfunction + +function! TearDown_Test_AutoLayoutTerminalHoriz() + unlet s:vimspector_ui_mode + call vimspector#test#setup#PopOption( 'lines' ) + call vimspector#test#setup#PopOption( 'columns' ) +endfunction + +function! SetUp_Test_AutoLayoutTerminalVertVert() + let s:vimspector_ui_mode = 'auto' + " Not wide enough to go horizontal, but wide enough to put the terminal and + " code vertically split + call vimspector#test#setup#PushOption( 'columns', 80 ) + call vimspector#test#setup#PushOption( 'lines', 50 ) +endfunction + +function! Test_AutoLayoutTerminalVertVert() + call s:StartDebugging() + + call vimspector#StepOver() + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( s:fn, 25, 1 ) + + call assert_equal( 'vertical', g:vimspector_session_windows.mode ) + call assert_equal( + \ [ 'col', [ + \ [ 'row', [ + \ [ 'leaf', g:vimspector_session_windows.variables ], + \ [ 'leaf', g:vimspector_session_windows.watches ], + \ [ 'leaf', g:vimspector_session_windows.stack_trace ], + \ ] ], + \ [ 'leaf', g:vimspector_session_windows.code ], + \ [ 'leaf', g:vimspector_session_windows.terminal ], + \ [ 'leaf', g:vimspector_session_windows.output ], + \ ] ], + \ winlayout( g:vimspector_session_windows.tabpage ) ) + + call vimspector#test#setup#Reset() + %bwipe! +endfunction + +function! TearDown_Test_AutoLayoutTerminalVertVert() + unlet s:vimspector_ui_mode + call vimspector#test#setup#PopOption( 'lines' ) + call vimspector#test#setup#PopOption( 'columns' ) +endfunction + + +function! Test_CloseVariables() + call s:StartDebugging() + + call win_execute( g:vimspector_session_windows.variables, 'q' ) + call vimspector#StepOver() + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( s:fn, 25, 1 ) + + call assert_equal( + \ [ 'row', [ + \ [ 'col', [ + \ [ 'leaf', g:vimspector_session_windows.watches ], + \ [ 'leaf', g:vimspector_session_windows.stack_trace ], + \ ] ], + \ [ 'col', [ + \ [ 'row', [ + \ [ 'leaf', g:vimspector_session_windows.code ], + \ [ 'leaf', g:vimspector_session_windows.terminal ], + \ ] ], + \ [ 'leaf', g:vimspector_session_windows.output ], + \ ] ] + \ ] ], + \ winlayout( g:vimspector_session_windows.tabpage ) ) + + call vimspector#test#setup#Reset() + %bwipe! +endfunction + +function! Test_CloseWatches() + call s:StartDebugging() + + call win_execute( g:vimspector_session_windows.watches, 'q' ) + call vimspector#StepOver() + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( s:fn, 25, 1 ) + + " Add a wtch + call vimspector#AddWatch( 't' ) + call vimspector#StepOver() + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( s:fn, 26, 1 ) + + call assert_equal( + \ [ 'row', [ + \ [ 'col', [ + \ [ 'leaf', g:vimspector_session_windows.variables ], + \ [ 'leaf', g:vimspector_session_windows.stack_trace ], + \ ] ], + \ [ 'col', [ + \ [ 'row', [ + \ [ 'leaf', g:vimspector_session_windows.code ], + \ [ 'leaf', g:vimspector_session_windows.terminal ], + \ ] ], + \ [ 'leaf', g:vimspector_session_windows.output ], + \ ] ] + \ ] ], + \ winlayout( g:vimspector_session_windows.tabpage ) ) + + " Replace the variables view with a watches view! + call win_execute( g:vimspector_session_windows.variables, + \ 'bu vimspector.Watches' ) + + " Delete a watch expression + call win_gotoid( g:vimspector_session_windows.variables ) + call setpos( '.', [ 0, 3, 1 ] ) + call feedkeys( "\", 'xt' ) + + call WaitForAssert( {-> + \ assert_equal( + \ [ + \ 'Watches: ----', + \ ], + \ getbufline( winbufnr( g:vimspector_session_windows.variables ), + \ 1, + \ '$' ) + \ ) + \ } ) + + + call vimspector#StepInto() + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( s:fn, 13, 1 ) + call vimspector#StepOver() + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( s:fn, 14, 1 ) + call vimspector#AddWatch( 'i' ) + + call WaitForAssert( {-> + \ assert_equal( + \ [ + \ 'Watches: ----', + \ 'Expression: i', + \ ' *- Result: 0', + \ ], + \ getbufline( winbufnr( g:vimspector_session_windows.variables ), + \ 1, + \ '$' ) + \ ) + \ } ) + + call vimspector#AddWatch( 'i+1' ) + + call WaitForAssert( {-> + \ assert_equal( + \ [ + \ 'Watches: ----', + \ 'Expression: i', + \ ' - Result: 0', + \ 'Expression: i+1', + \ ' *- Result: 1', + \ ], + \ getbufline( winbufnr( g:vimspector_session_windows.variables ), + \ 1, + \ '$' ) + \ ) + \ } ) + + call vimspector#AddWatch( 'i+2' ) + + call WaitForAssert( {-> + \ assert_equal( + \ [ + \ 'Watches: ----', + \ 'Expression: i', + \ ' - Result: 0', + \ 'Expression: i+1', + \ ' - Result: 1', + \ 'Expression: i+2', + \ ' *- Result: 2', + \ ], + \ getbufline( winbufnr( g:vimspector_session_windows.variables ), + \ 1, + \ '$' ) + \ ) + \ } ) + + " Delete that middle watch + call win_gotoid( g:vimspector_session_windows.variables ) + call setpos( '.', [ 0, 4, 1 ] ) + call vimspector#DeleteWatch() + + call WaitForAssert( {-> + \ assert_equal( + \ [ + \ 'Watches: ----', + \ 'Expression: i', + \ ' - Result: 0', + \ 'Expression: i+2', + \ ' *- Result: 2', + \ ], + \ getbufline( winbufnr( g:vimspector_session_windows.variables ), + \ 1, + \ '$' ) + \ ) + \ } ) + + call vimspector#StepOver() + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( s:fn, 15, 1 ) + call WaitForAssert( {-> + \ assert_equal( + \ [ + \ 'Watches: ----', + \ 'Expression: i', + \ ' - Result: 0', + \ 'Expression: i+2', + \ ' - Result: 2', + \ ], + \ getbufline( winbufnr( g:vimspector_session_windows.variables ), + \ 1, + \ '$' ) + \ ) + \ } ) + + " Delete the top watch + call win_gotoid( g:vimspector_session_windows.variables ) + call setpos( '.', [ 0, 3, 1 ] ) + call vimspector#DeleteWatch() + + call vimspector#StepOver() + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( s:fn, 13, 1 ) + call vimspector#StepOver() + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( s:fn, 14, 1 ) + call WaitForAssert( {-> + \ assert_equal( + \ [ + \ 'Watches: ----', + \ 'Expression: i+2', + \ ' *- Result: 3', + \ ], + \ getbufline( winbufnr( g:vimspector_session_windows.variables ), + \ 1, + \ '$' ) + \ ) + \ } ) + call vimspector#test#setup#Reset() + %bwipe! +endfunction + +function! Test_CloseStackTrace() + call s:StartDebugging() + + call win_execute( g:vimspector_session_windows.stack_trace, 'q' ) + call vimspector#StepOver() + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( s:fn, 25, 1 ) + + call assert_equal( + \ [ 'row', [ + \ [ 'col', [ + \ [ 'leaf', g:vimspector_session_windows.variables ], + \ [ 'leaf', g:vimspector_session_windows.watches ], + \ ] ], + \ [ 'col', [ + \ [ 'row', [ + \ [ 'leaf', g:vimspector_session_windows.code ], + \ [ 'leaf', g:vimspector_session_windows.terminal ], + \ ] ], + \ [ 'leaf', g:vimspector_session_windows.output ], + \ ] ] + \ ] ], + \ winlayout( g:vimspector_session_windows.tabpage ) ) + + call vimspector#test#setup#Reset() + %bwipe! +endfunction + +function! Test_CloseOutput() + call s:StartDebugging() + + call win_execute( g:vimspector_session_windows.output, 'q' ) + call vimspector#StepOver() + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( s:fn, 25, 1 ) + + call assert_equal( + \ [ 'row', [ + \ [ 'col', [ + \ [ 'leaf', g:vimspector_session_windows.variables ], + \ [ 'leaf', g:vimspector_session_windows.watches ], + \ [ 'leaf', g:vimspector_session_windows.stack_trace ], + \ ] ], + \ [ 'leaf', g:vimspector_session_windows.code ], + \ [ 'leaf', g:vimspector_session_windows.terminal ], + \ ] ], + \ winlayout( g:vimspector_session_windows.tabpage ) ) + + call vimspector#test#setup#Reset() + %bwipe! +endfunction + +function! Test_CloseOutput_Early() + augroup TestCustomUI + au! + au User VimspectorUICreated + \ call win_execute( g:vimspector_session_windows.output, 'q' ) + augroup END + + call s:StartDebugging() + + call vimspector#StepOver() + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( s:fn, 25, 1 ) + + call assert_equal( + \ [ 'row', [ + \ [ 'col', [ + \ [ 'leaf', g:vimspector_session_windows.variables ], + \ [ 'leaf', g:vimspector_session_windows.watches ], + \ [ 'leaf', g:vimspector_session_windows.stack_trace ], + \ ] ], + \ [ 'leaf', g:vimspector_session_windows.code ], + \ [ 'leaf', g:vimspector_session_windows.terminal ], + \ ] ], + \ winlayout( g:vimspector_session_windows.tabpage ) ) + + " Open it again! + let g:vimspector_bottombar_height = 5 + VimspectorShowOutput Console + call assert_equal( + \ [ 'col', [ + \ [ 'row', [ + \ [ 'col', [ + \ [ 'leaf', g:vimspector_session_windows.variables ], + \ [ 'leaf', g:vimspector_session_windows.watches ], + \ [ 'leaf', g:vimspector_session_windows.stack_trace ], + \ ] ], + \ [ 'leaf', g:vimspector_session_windows.code ], + \ [ 'leaf', g:vimspector_session_windows.terminal ], + \ ] ], + \ [ 'leaf', g:vimspector_session_windows.output ] + \ ] ], + \ winlayout( g:vimspector_session_windows.tabpage ) ) + + " The actual height reported is the number of lines visible. The WinBar takes + " 1 screen row, so g:vimspector_bottombar_height -1 + call assert_equal( 4, winheight( g:vimspector_session_windows.output ) ) + + call vimspector#StepOver() + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( s:fn, 26, 1 ) + + au! TestCustomUI + call vimspector#test#setup#Reset() + %bwipe! +endfunction + + +function! Test_CustomUI() + augroup TestCustomUI + au! + au User VimspectorUICreated + \ call win_execute( g:vimspector_session_windows.watches, 'q' ) + augroup END + + call s:StartDebugging() + + call vimspector#StepOver() + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( s:fn, 25, 1 ) + + " Add a watch + call vimspector#AddWatch( 't' ) + call vimspector#StepOver() + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( s:fn, 26, 1 ) + + call assert_equal( + \ [ 'row', [ + \ [ 'col', [ + \ [ 'leaf', g:vimspector_session_windows.variables ], + \ [ 'leaf', g:vimspector_session_windows.stack_trace ], + \ ] ], + \ [ 'col', [ + \ [ 'row', [ + \ [ 'leaf', g:vimspector_session_windows.code ], + \ [ 'leaf', g:vimspector_session_windows.terminal ], + \ ] ], + \ [ 'leaf', g:vimspector_session_windows.output ], + \ ] ] + \ ] ], + \ winlayout( g:vimspector_session_windows.tabpage ) ) + + au! TestCustomUI + call vimspector#test#setup#Reset() + %bwipe! +endfunction + + +function! s:CustomWinBar() + call win_gotoid( g:vimspector_session_windows.code) + aunmenu WinBar + nnoremenu WinBar.▷\ ᶠ⁵ :call vimspector#Continue() + nnoremenu WinBar.↷\ ᶠ¹⁰ :call vimspector#StepOver() + nnoremenu WinBar.↓\ ᶠ¹¹ :call vimspector#StepInto() + nnoremenu WinBar.↑\ ˢᶠ¹¹ :call vimspector#StepOut() + nnoremenu WinBar.❘❘\ ᶠ⁶ :call vimspector#Pause() + nnoremenu WinBar.□\ ˢᶠ⁵ :call vimspector#Stop() + nnoremenu WinBar.⟲\ ᶜˢᶠ⁵ :call vimspector#Restart() + nnoremenu WinBar.✕\ ᶠ⁸ :call vimspector#Reset() +endfunction + + +function! Test_CustomWinBar() + augroup TestCustomWinBar + au! + au User VimspectorUICreated call s:CustomWinBar() + augroup END + + call s:StartDebugging() + call assert_equal( + \ ['▷ ᶠ⁵', '↷ ᶠ¹⁰', '↓ ᶠ¹¹', '↑ ˢᶠ¹¹', '❘❘ ᶠ⁶', '□ ˢᶠ⁵', '⟲ ᶜˢᶠ⁵', '✕ ᶠ⁸'], + \ menu_info( 'WinBar' ).submenus ) + + au! TestCustomWinBar + call vimspector#test#setup#Reset() + %bwipe! +endfunction + +function! Test_VimspectorJumpedToFrame() + let s:ended = 0 + let s:au_visited_buffers = {} + + augroup TestVimspectorJumpedToFrame + au! + au User VimspectorJumpedToFrame + \ let s:au_visited_buffers[ bufname() ] = get( s:au_visited_buffers, + \ bufname(), + \ 0 ) + 1 + au User VimspectorDebugEnded + \ let s:ended = 1 + augroup END + + lcd ../support/test/python/multiple_files + edit moo.py + + let moo = 'moo.py' + let cow = getcwd() . '/cow.py' + + call vimspector#SetLineBreakpoint( 'moo.py', 13 ) + call vimspector#Launch() + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( 'moo.py', 1, 1 ) + call vimspector#test#signs#AssertPCIsAtLineInBuffer( 'moo.py', 1 ) + let expected = {} + let expected[ moo ] = 1 + call assert_equal( expected, s:au_visited_buffers ) + + call vimspector#Continue() + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( 'moo.py', 13, 1 ) + call vimspector#test#signs#AssertPCIsAtLineInBuffer( 'moo.py', 13 ) + let expected[ moo ] += 1 + call assert_equal( expected, s:au_visited_buffers ) + + call vimspector#SetLineBreakpoint( 'cow.py', 2 ) + call vimspector#Continue() + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( 'cow.py', 2, 1 ) + call vimspector#test#signs#AssertPCIsAtLineInBuffer( 'cow.py', 2 ) + let expected[ cow ] = 1 + call assert_equal( expected, s:au_visited_buffers ) + + VimspectorReset + call WaitForAssert( { -> assert_equal( s:ended, 1 ) } ) + + au! TestVimspectorJumpedToFrame + unlet! s:au_visited_buffers + unlet! s:ended + + call vimspector#test#setup#Reset() + lcd - + %bwipe! +endfunction diff --git a/tests/utils.test.vim b/tests/utils.test.vim new file mode 100644 index 0000000..ad485ce --- /dev/null +++ b/tests/utils.test.vim @@ -0,0 +1,28 @@ +function! SetUp() + call vimspector#test#setup#SetUpWithMappings( v:none ) + py3 import vim + py3 __import__( 'vimspector' ) +endfunction + +function! ClearDown() + call vimspector#test#setup#ClearDown() +endfunction + +function! s:RunPyFile( file_name ) + redir => py_output + try + let v:errmsg = '' + silent! execute 'py3file python/' .. a:file_name + finally + redir END + call TestLog( [ a:file_name .. ' output:' ] + split( py_output, '\n' ) ) + endtry + + if v:errmsg !=# '' + call assert_report( v:errmsg ) + endif +endfunction + +function! Test_ExpandReferencesInDict() + call s:RunPyFile( 'Test_ExpandReferencesInDict.py' ) +endfunction diff --git a/tests/variables.test.vim b/tests/variables.test.vim new file mode 100644 index 0000000..c00fb7f --- /dev/null +++ b/tests/variables.test.vim @@ -0,0 +1,1135 @@ +let s:fn='../support/test/python/simple_python/main.py' + +function! SetUp() + call vimspector#test#setup#SetUpWithMappings( 'HUMAN' ) +endfunction + +function! ClearDown() + call vimspector#test#setup#ClearDown() +endfunction + +function! s:StartDebugging( ... ) + if a:0 == 0 + let config = #{ + \ fn: s:fn, + \ line: 23, + \ col: 1, + \ launch: #{ configuration: 'run' } + \ } + else + let config = a:1 + endif + + execute 'edit' config.fn + call setpos( '.', [ 0, config.line, config.col ] ) + call vimspector#ToggleBreakpoint() + call vimspector#LaunchWithSettings( config.launch ) + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( + \ config.fn, + \ config.line, + \ config.col ) +endfunction + +function! Test_SimpleWatches() + call s:StartDebugging() + + call vimspector#StepOver() + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( s:fn, 25, 1 ) + + " Add a wtch + call vimspector#AddWatch( 't' ) + call vimspector#StepOver() + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( s:fn, 26, 1 ) + + " Delete a watch expression + call win_gotoid( g:vimspector_session_windows.watches ) + call setpos( '.', [ 0, 3, 1 ] ) + call feedkeys( "\", 'xt' ) + + call WaitForAssert( {-> + \ assert_equal( + \ [ + \ 'Watches: ----', + \ ], + \ getbufline( winbufnr( g:vimspector_session_windows.watches ), + \ 1, + \ '$' ) + \ ) + \ } ) + + call assert_equal( 'python', + \ getbufvar( + \ winbufnr( g:vimspector_session_windows.watches ), + \ '&syntax' ) ) + + call vimspector#StepInto() + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( s:fn, 13, 1 ) + call vimspector#StepOver() + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( s:fn, 14, 1 ) + call vimspector#AddWatch( 'i' ) + + call WaitForAssert( {-> + \ assert_equal( + \ [ + \ 'Watches: ----', + \ 'Expression: i', + \ ' *- Result: 0', + \ ], + \ getbufline( winbufnr( g:vimspector_session_windows.watches ), + \ 1, + \ '$' ) + \ ) + \ } ) + + call vimspector#AddWatch( 'i+1' ) + + call WaitForAssert( {-> + \ assert_equal( + \ [ + \ 'Watches: ----', + \ 'Expression: i', + \ ' - Result: 0', + \ 'Expression: i+1', + \ ' *- Result: 1', + \ ], + \ getbufline( winbufnr( g:vimspector_session_windows.watches ), + \ 1, + \ '$' ) + \ ) + \ } ) + + call vimspector#AddWatch( 'i+2' ) + + call WaitForAssert( {-> + \ assert_equal( + \ [ + \ 'Watches: ----', + \ 'Expression: i', + \ ' - Result: 0', + \ 'Expression: i+1', + \ ' - Result: 1', + \ 'Expression: i+2', + \ ' *- Result: 2', + \ ], + \ getbufline( winbufnr( g:vimspector_session_windows.watches ), + \ 1, + \ '$' ) + \ ) + \ } ) + + " Delete that middle watch + call win_gotoid( g:vimspector_session_windows.watches ) + call setpos( '.', [ 0, 4, 1 ] ) + call vimspector#DeleteWatch() + + call WaitForAssert( {-> + \ assert_equal( + \ [ + \ 'Watches: ----', + \ 'Expression: i', + \ ' - Result: 0', + \ 'Expression: i+2', + \ ' *- Result: 2', + \ ], + \ getbufline( winbufnr( g:vimspector_session_windows.watches ), + \ 1, + \ '$' ) + \ ) + \ } ) + + call vimspector#StepOver() + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( s:fn, 15, 1 ) + call WaitForAssert( {-> + \ assert_equal( + \ [ + \ 'Watches: ----', + \ 'Expression: i', + \ ' - Result: 0', + \ 'Expression: i+2', + \ ' - Result: 2', + \ ], + \ getbufline( winbufnr( g:vimspector_session_windows.watches ), + \ 1, + \ '$' ) + \ ) + \ } ) + + " Delete the top watch + call win_gotoid( g:vimspector_session_windows.watches ) + call setpos( '.', [ 0, 3, 1 ] ) + call vimspector#DeleteWatch() + + call vimspector#StepOver() + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( s:fn, 13, 1 ) + call vimspector#StepOver() + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( s:fn, 14, 1 ) + call WaitForAssert( {-> + \ assert_equal( + \ [ + \ 'Watches: ----', + \ 'Expression: i+2', + \ ' *- Result: 3', + \ ], + \ getbufline( winbufnr( g:vimspector_session_windows.watches ), + \ 1, + \ '$' ) + \ ) + \ } ) + call vimspector#test#setup#Reset() + %bwipe! +endfunction + +function! Test_ExpandVariables() + let fn = 'testdata/cpp/simple/struct.cpp' + call s:StartDebugging( #{ fn: fn, line: 24, col: 1, launch: #{ + \ configuration: 'run-to-breakpoint' + \ } } ) + + " Make sure the Test t is initialised + call vimspector#StepOver() + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( fn, 26, 1 ) + + call WaitForAssert( {-> + \ assert_equal( + \ [ + \ '- Scope: Locals', + \ ' *+ t (Test): {...}', + \ '+ Scope: Registers', + \ ], + \ getbufline( winbufnr( g:vimspector_session_windows.variables ), + \ 1, + \ '$' ) + \ ) + \ } ) + call assert_equal( 'cpp', + \ getbufvar( + \ winbufnr( g:vimspector_session_windows.variables ), + \ '&syntax' ) ) + + " Expand + call win_gotoid( g:vimspector_session_windows.variables ) + call setpos( '.', [ 0, 2, 1 ] ) + call feedkeys( "\", 'xt' ) + + call WaitForAssert( {-> + \ AssertMatchList( + \ [ + \ '- Scope: Locals', + \ ' \*- t (Test): {...}', + \ ' \*- i (int): 0', + \ ' \*- c (char): 0 ''\\0\{1,3}''', + \ ' \*- fffff (float): 0', + \ ' \*+ another_test (AnotherTest):\( {...}\)\?', + \ '+ Scope: Registers', + \ ], + \ getbufline( winbufnr( g:vimspector_session_windows.variables ), + \ 1, + \ '$' ) + \ ) + \ } ) + + " Step - stays expanded + call vimspector#StepOver() + call WaitForAssert( {-> + \ AssertMatchList( + \ [ + \ '- Scope: Locals', + \ ' - t (Test): {...}', + \ ' \*- i (int): 1', + \ ' - c (char): 0 ''\\0\{1,3}''', + \ ' - fffff (float): 0', + \ ' + another_test (AnotherTest):\( {...}\)\?', + \ '+ Scope: Registers', + \ ], + \ getbufline( winbufnr( g:vimspector_session_windows.variables ), + \ 1, + \ '$' ) + \ ) + \ } ) + + " Collapse + call win_gotoid( g:vimspector_session_windows.variables ) + call setpos( '.', [ 0, 2, 1 ] ) + call feedkeys( "\", 'xt' ) + call WaitForAssert( {-> + \ assert_equal( + \ [ + \ '- Scope: Locals', + \ ' + t (Test): {...}', + \ '+ Scope: Registers', + \ ], + \ getbufline( winbufnr( g:vimspector_session_windows.variables ), + \ 1, + \ '$' ) + \ ) + \ } ) + + call vimspector#StepOver() + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( fn, 28, 1 ) + call WaitForAssert( {-> + \ assert_equal( + \ [ + \ '- Scope: Locals', + \ ' + t (Test): {...}', + \ '+ Scope: Registers', + \ ], + \ getbufline( winbufnr( g:vimspector_session_windows.variables ), + \ 1, + \ '$' ) + \ ) + \ } ) + + call win_gotoid( g:vimspector_session_windows.variables ) + call setpos( '.', [ 0, 2, 1 ] ) + call feedkeys( "\", 'xt' ) + call WaitForAssert( {-> + \ AssertMatchList( + \ [ + \ '- Scope: Locals', + \ ' - t (Test): {...}', + \ ' \*- i (int): 1', + \ ' \*- c (char): 99 ''c''', + \ ' \*- fffff (float): 0', + \ ' \*+ another_test (AnotherTest):\( {...}\)\?', + \ '+ Scope: Registers', + \ ], + \ getbufline( winbufnr( g:vimspector_session_windows.variables ), + \ 1, + \ '$' ) + \ ) + \ } ) + + " Collapse the 'inexpensive' scope and see that it stays collapsed + " Exapand - see that the changed value is highlighted + call win_gotoid( g:vimspector_session_windows.variables ) + call setpos( '.', [ 0, 1, 1 ] ) + call feedkeys( "\", 'xt' ) + call WaitForAssert( {-> + \ assert_equal( + \ [ + \ '+ Scope: Locals', + \ '+ Scope: Registers', + \ ], + \ getbufline( winbufnr( g:vimspector_session_windows.variables ), + \ 1, + \ '$' ) + \ ) + \ } ) + + " Stays collpased through step + call vimspector#StepOver() + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( fn, 30, 1 ) + call WaitForAssert( {-> + \ assert_equal( + \ [ + \ '+ Scope: Locals', + \ '+ Scope: Registers', + \ ], + \ getbufline( winbufnr( g:vimspector_session_windows.variables ), + \ 1, + \ '$' ) + \ ) + \ } ) + + " Cpptools keeps the same "Locals" scope, so it stays collapsed even throught + " step-in + call vimspector#StepInto() + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( fn, 18, 1 ) + call WaitForAssert( {-> + \ assert_equal( + \ [ + \ '+ Scope: Locals', + \ '+ Scope: Registers', + \ ], + \ getbufline( winbufnr( g:vimspector_session_windows.variables ), + \ 1, + \ '$' ) + \ ) + \ } ) + + call vimspector#test#setup#Reset() + %bwipe! +endfunction + +function! Test_ExpandWatch() + let fn = 'testdata/cpp/simple/struct.cpp' + call s:StartDebugging( #{ fn: fn, line: 24, col: 1, launch: #{ + \ configuration: 'run-to-breakpoint' + \ } } ) + + " Make sure the Test t is initialised + call vimspector#StepOver() + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( fn, 26, 1 ) + + call win_gotoid( g:vimspector_session_windows.watches ) + call feedkeys( "it\", 'xt' ) + + call WaitForAssert( {-> + \ assert_equal( + \ [ + \ 'Watches: ----', + \ 'Expression: t', + \ ' *+ Result: {...}', + \ ], + \ getbufline( winbufnr( g:vimspector_session_windows.watches ), + \ 1, + \ '$' ) + \ ) + \ } ) + call assert_equal( 'cpp', + \ getbufvar( + \ winbufnr( g:vimspector_session_windows.watches ), + \ '&syntax' ) ) + + " Expand + call win_gotoid( g:vimspector_session_windows.watches ) + call setpos( '.', [ 0, 3, 1 ] ) + call feedkeys( "\", 'xt' ) + + call WaitForAssert( {-> + \ AssertMatchList( + \ [ + \ 'Watches: ----', + \ 'Expression: t', + \ ' \*- Result: {...}', + \ ' \*- i (int): 0', + \ ' \*- c (char): 0 ''\\0\{1,3}''', + \ ' \*- fffff (float): 0', + \ ' \*+ another_test (AnotherTest):\( {...}\)\?', + \ ], + \ getbufline( winbufnr( g:vimspector_session_windows.watches ), + \ 1, + \ '$' ) + \ ) + \ } ) + + " Step - stays expanded + call vimspector#StepOver() + call WaitForAssert( {-> + \ AssertMatchList( + \ [ + \ 'Watches: ----', + \ 'Expression: t', + \ ' - Result: {...}', + \ ' \*- i (int): 1', + \ ' - c (char): 0 ''\\0\{1,3}''', + \ ' - fffff (float): 0', + \ ' + another_test (AnotherTest):\( {...}\)\?', + \ ], + \ getbufline( winbufnr( g:vimspector_session_windows.watches ), + \ 1, + \ '$' ) + \ ) + \ } ) + + " Collapse + call win_gotoid( g:vimspector_session_windows.watches ) + call setpos( '.', [ 0, 3, 1 ] ) + call feedkeys( "\", 'xt' ) + call WaitForAssert( {-> + \ assert_equal( + \ [ + \ 'Watches: ----', + \ 'Expression: t', + \ ' + Result: {...}', + \ ], + \ getbufline( winbufnr( g:vimspector_session_windows.watches ), + \ 1, + \ '$' ) + \ ) + \ } ) + + call vimspector#StepOver() + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( fn, 28, 1 ) + call WaitForAssert( {-> + \ assert_equal( + \ [ + \ 'Watches: ----', + \ 'Expression: t', + \ ' + Result: {...}', + \ ], + \ getbufline( winbufnr( g:vimspector_session_windows.watches ), + \ 1, + \ '$' ) + \ ) + \ } ) + + call win_gotoid( g:vimspector_session_windows.watches ) + call setpos( '.', [ 0, 3, 1 ] ) + call feedkeys( "\", 'xt' ) + call WaitForAssert( {-> + \ AssertMatchList( + \ [ + \ 'Watches: ----', + \ 'Expression: t', + \ ' - Result: {...}', + \ ' - i (int): 1', + \ ' - c (char): 99 ''c''', + \ ' - fffff (float): 0', + \ ' + another_test (AnotherTest):\( {...}\)\?', + \ ], + \ getbufline( winbufnr( g:vimspector_session_windows.watches ), + \ 1, + \ '$' ) + \ ) + \ } ) + + call vimspector#test#setup#Reset() + %bwipe! +endfunction + + +function Test_EvaluateConsole() + let fn = 'testdata/cpp/simple/struct.cpp' + call s:StartDebugging( #{ fn: fn, line: 24, col: 1, launch: #{ + \ configuration: 'run-to-breakpoint' + \ } } ) + + " Make sure the Test t is initialised + call vimspector#StepOver() + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( fn, 26, 1 ) + call vimspector#StepOver() + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( fn, 27, 1 ) + + VimspectorEval t.i + call assert_equal( bufnr( 'vimspector.Console' ), + \ winbufnr( g:vimspector_session_windows.output ) ) + call WaitForAssert( {-> + \ assert_equal( + \ [ + \ '1' + \ ], + \ getbufline( bufnr( 'vimspector.Console' ), '$', '$' ) + \ ) + \ } ) + + let len = getbufinfo( 'vimspector.Console' )[ 0 ].linecount + + call WaitForAssert( {-> + \ assert_equal( + \ [ + \ 'Evaluating: t.i', + \ '1' + \ ], + \ getbufline( bufnr( 'vimspector.Console' ), len-1, '$' ) + \ ) + \ } ) + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( + \ 'vimspector.Console', len, v:null ) + + call vimspector#test#setup#Reset() + %bwipe! +endfunction + + +function Test_EvaluateInput() + let fn = 'testdata/cpp/simple/struct.cpp' + call s:StartDebugging( #{ fn: fn, line: 24, col: 1, launch: #{ + \ configuration: 'run-to-breakpoint' + \ } } ) + + " Make sure the Test t is initialised + call vimspector#StepOver() + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( fn, 26, 1 ) + call vimspector#StepOver() + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( fn, 27, 1 ) + + VimspectorEval -exec print (int) printf("hello") + + call assert_equal( bufnr( 'vimspector.Console' ), + \ winbufnr( g:vimspector_session_windows.output ) ) + call WaitForAssert( {-> + \ assert_equal( + \ [ + \ '' + \ ], + \ getbufline( bufnr( 'vimspector.Console' ), '$', '$' ) + \ ) + \ } ) + + let len = getbufinfo( 'vimspector.Console' )[ 0 ].linecount + + call WaitForAssert( {-> + \ assert_equal( + \ [ + \ 'Evaluating: -exec print (int) printf("hello")', + \ ], + \ getbufline( bufnr( 'vimspector.Console' ), len-2, len-2 ) + \ ) + \ } ) + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( + \ 'vimspector.Console', len, v:null ) + + call vimspector#test#setup#Reset() + %bwipe! +endfunction + + +function Test_EvaluatePromptConsole() + let fn = 'testdata/cpp/simple/struct.cpp' + call s:StartDebugging( #{ fn: fn, line: 24, col: 1, launch: #{ + \ configuration: 'run-to-breakpoint' + \ } } ) + + " Make sure the Test t is initialised + call vimspector#StepOver() + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( fn, 26, 1 ) + call vimspector#StepOver() + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( fn, 27, 1 ) + + VimspectorShowOutput + call assert_equal( bufnr( 'vimspector.Console' ), + \ winbufnr( g:vimspector_session_windows.output ) ) + + call feedkeys( "it.i\", 'xt' ) + call WaitForAssert( {-> + \ assert_equal( + \ [ + \ '1' + \ ], + \ getbufline( bufnr( 'vimspector.Console' ), '$', '$' ) + \ ) + \ } ) + + let len = getbufinfo( 'vimspector.Console' )[ 0 ].linecount + + call WaitForAssert( {-> + \ assert_equal( + \ [ + \ '> t.i', + \ '', + \ '1' + \ ], + \ getbufline( bufnr( 'vimspector.Console' ), len-2, '$' ) + \ ) + \ } ) + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( + \ 'vimspector.Console', len, v:null ) + + call vimspector#test#setup#Reset() + %bwipe! +endfunction + +function! Test_EvaluateFailure() + call s:StartDebugging() + + " Add a wtch + call vimspector#AddWatch( 'test' ) + call WaitForAssert( {-> + \ AssertMatchList( + \ [ + \ 'Watches: ----', + \ 'Expression: test', + \ " *- Result: NameError: name 'test' is not defined", + \ ], + \ getbufline( winbufnr( g:vimspector_session_windows.watches ), + \ 1, + \ '$' ) + \ ) + \ } ) + + VimspectorEval test + call assert_equal( bufnr( 'vimspector.Console' ), + \ winbufnr( g:vimspector_session_windows.output ) ) + call WaitForAssert( {-> + \ assert_equal( + \ [ + \ "NameError: name 'test' is not defined" + \ ], + \ getbufline( bufnr( 'vimspector.Console' ), '$', '$' ) + \ ) + \ } ) + + call vimspector#test#setup#Reset() + %bwipe! +endfunction + +function! Test_VariableEval() + let fn = 'testdata/cpp/simple/struct.cpp' + call s:StartDebugging( #{ fn: fn, line: 24, col: 1, launch: #{ + \ configuration: 'run-to-breakpoint' + \ } } ) + + call vimspector#StepOver() + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( fn, 26, 1 ) + + " leader is , + xmap d VimspectorBalloonEval + nmap d VimspectorBalloonEval + + "evaluate the prev line + call setpos( '.', [ 0, 24, 8 ] ) + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( fn, 24, 8 ) + call feedkeys( ',d', 'xt' ) + + call WaitForAssert( {-> + \ assert_notequal( v:none, g:vimspector_session_windows.eval ) + \ } ) + + call WaitForAssert( {-> + \ AssertMatchList( + \ [ + \ '{...}', + \ ' - i: 0', + \ ' - c: 0 ''\\0\{1,3}''', + \ ' - fffff: 0', + \ ' + another_test: ', + \ ], + \ getbufline( winbufnr( g:vimspector_session_windows.eval ), + \ 1, + \ '$' ) + \ ) + \ } ) + + "Close + call feedkeys( "\", 'xt' ) + + call WaitForAssert( {-> + \ assert_equal( v:none, g:vimspector_session_windows.eval ) + \ } ) + + " test selection + call setpos( '.', [ 0, 24, 8 ] ) + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( fn, 24, 8 ) + + call feedkeys( 'viw,d', 'xt' ) + + call WaitForAssert( {-> + \ assert_notequal( v:none, g:vimspector_session_windows.eval ) + \ } ) + + call WaitForAssert( {-> + \ AssertMatchList( + \ [ + \ '{...}', + \ ' - i: 0', + \ ' - c: 0 ''\\0\{1,3}''', + \ ' - fffff: 0', + \ ' + another_test: ', + \ ], + \ getbufline( winbufnr( g:vimspector_session_windows.eval ), + \ 1, + \ '$' ) + \ ) + \ } ) + + "Close + call feedkeys( "\", 'xt' ) + + call WaitForAssert( {-> + \ assert_equal( v:none, g:vimspector_session_windows.eval ) + \ } ) + + " Get back to normal mode + call feedkeys( "\", 'xt' ) + + " Evaluation error + call setpos( '.', [ 0, 25, 1 ] ) + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( fn, 25, 1 ) + call feedkeys( ',d', 'xt' ) + + call WaitForAssert( {-> + \ assert_notequal( v:none, g:vimspector_session_windows.eval ) + \ } ) + + call WaitForAssert( {-> + \ AssertMatchList( + \ [ + \ 'Evaluation error', + \ ], + \ getbufline( winbufnr( g:vimspector_session_windows.eval ), + \ 1, + \ '$' ) + \ ) + \ } ) + + "Close + call feedkeys( "\", 'xt' ) + + call WaitForAssert( {-> + \ assert_equal( v:none, g:vimspector_session_windows.eval ) + \ } ) + + call vimspector#test#setup#Reset() + %bwipe! +endfunction + +function! Test_VariableEvalExpand() + let fn = 'testdata/cpp/simple/struct.cpp' + call s:StartDebugging( #{ fn: fn, line: 24, col: 1, launch: #{ + \ configuration: 'run-to-breakpoint' + \ } } ) + + call vimspector#StepOver() + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( fn, 26, 1 ) + + " leader is , + xmap d VimspectorBalloonEval + nmap d VimspectorBalloonEval + + "evaluate the prev line + call setpos( '.', [ 0, 24, 8 ] ) + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( fn, 24, 8 ) + call feedkeys( ',d', 'xt' ) + + call WaitForAssert( {-> + \ assert_notequal( v:none, g:vimspector_session_windows.eval ) + \ } ) + + call WaitForAssert( {-> + \ AssertMatchList( + \ [ + \ '{...}', + \ ' - i: 0', + \ ' - c: 0 ''\\0\{1,3}''', + \ ' - fffff: 0', + \ ' + another_test: ', + \ ], + \ getbufline( winbufnr( g:vimspector_session_windows.eval ), + \ 1, + \ '$' ) + \ ) + \ } ) + + " Expand + call feedkeys( "jjjj\", 'xt' ) + + call WaitForAssert( {-> + \ AssertMatchList( + \ [ + \ '{...}', + \ ' - i: 0', + \ ' - c: 0 ''\\0\{1,3}''', + \ ' - fffff: 0', + \ ' - another_test: ', + \ ' - choo: 0 ''\\0\{1,3}''', + \ ' + ints: ' + \ ], + \ getbufline( winbufnr( g:vimspector_session_windows.eval ), + \ 1, + \ '$' ) + \ ) + \ } ) + + "Collapse + call feedkeys( "\", 'xt' ) + + call WaitForAssert( {-> + \ AssertMatchList( + \ [ + \ '{...}', + \ ' - i: 0', + \ ' - c: 0 ''\\0\{1,3}''', + \ ' - fffff: 0', + \ ' + another_test: ', + \ ], + \ getbufline( winbufnr( g:vimspector_session_windows.eval ), + \ 1, + \ '$' ) + \ ) + \ } ) + + "Close + call feedkeys( "\", 'xt' ) + + call WaitForAssert( {-> + \ assert_equal( v:none, g:vimspector_session_windows.eval ) + \ } ) + + call vimspector#test#setup#Reset() + %bwipe! +endfunction + +function! Test_SetVariableValue_Local() + let fn = 'testdata/cpp/simple/struct.cpp' + call s:StartDebugging( #{ fn: fn, line: 24, col: 1, launch: #{ + \ configuration: 'run-to-breakpoint' + \ } } ) + + " Make sure the Test t is initialised + call vimspector#StepOver() + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( fn, 26, 1 ) + + call WaitForAssert( {-> + \ assert_equal( + \ [ + \ '- Scope: Locals', + \ ' *+ t (Test): {...}', + \ '+ Scope: Registers', + \ ], + \ getbufline( winbufnr( g:vimspector_session_windows.variables ), + \ 1, + \ '$' ) + \ ) + \ } ) + call assert_equal( 'cpp', + \ getbufvar( + \ winbufnr( g:vimspector_session_windows.variables ), + \ '&syntax' ) ) + + " Expand + call win_gotoid( g:vimspector_session_windows.variables ) + call setpos( '.', [ 0, 2, 1 ] ) + call feedkeys( "\", 'xt' ) + + call WaitForAssert( {-> + \ AssertMatchList( + \ [ + \ '- Scope: Locals', + \ ' \*- t (Test): {...}', + \ ' \*- i (int): 0', + \ ' \*- c (char): 0 ''\\0\{1,3}''', + \ ' \*- fffff (float): 0', + \ ' \*+ another_test (AnotherTest):\( {...}\)\?', + \ '+ Scope: Registers', + \ ], + \ getbufline( winbufnr( g:vimspector_session_windows.variables ), + \ 1, + \ '$' ) + \ ) + \ } ) + + call setpos( '.', [ 0, 3, 1 ] ) + + " We can't just fire the keys to the inpit prompt because we use inputsave() + " and inputrestore(), so mock that out and fire away. + py3 <\100\", "xt" )' ) +EOF + + call WaitForAssert( {-> + \ AssertMatchList( + \ [ + \ '- Scope: Locals', + \ ' \*- t (Test): {...}', + \ ' \*- i (int): 100', + \ ' \*- c (char): 0 ''\\0\{1,3}''', + \ ' \*- fffff (float): 0', + \ ' \*+ another_test (AnotherTest):\( {...}\)\?', + \ '+ Scope: Registers', + \ ], + \ getbufline( winbufnr( g:vimspector_session_windows.variables ), + \ 1, + \ '$' ) + \ ) + \ } ) + + " Now set it via the more comforable scripting interface + call vimspector#SetVariableValue( '1234' ) + + call WaitForAssert( {-> + \ AssertMatchList( + \ [ + \ '- Scope: Locals', + \ ' \*- t (Test): {...}', + \ ' \*- i (int): 1234', + \ ' \*- c (char): 0 ''\\0\{1,3}''', + \ ' \*- fffff (float): 0', + \ ' \*+ another_test (AnotherTest):\( {...}\)\?', + \ '+ Scope: Registers', + \ ], + \ getbufline( winbufnr( g:vimspector_session_windows.variables ), + \ 1, + \ '$' ) + \ ) + \ } ) + + " Something fails + call vimspector#SetVariableValue( 'this is invalid' ) + + call WaitForAssert( {-> + \ AssertMatchList( + \ [ + \ '- Scope: Locals', + \ ' \*- t (Test): {...}', + \ ' \*- i (int): 1234', + \ ' \*- c (char): 0 ''\\0\{1,3}''', + \ ' \*- fffff (float): 0', + \ ' \*+ another_test (AnotherTest):\( {...}\)\?', + \ '+ Scope: Registers', + \ ], + \ getbufline( winbufnr( g:vimspector_session_windows.variables ), + \ 1, + \ '$' ) + \ ) + \ } ) + + + call vimspector#test#setup#Reset() + %bwipe! +endfunction + +function! Test_SetVariableValue_Watch() + let fn = 'testdata/cpp/simple/struct.cpp' + call s:StartDebugging( #{ fn: fn, line: 24, col: 1, launch: #{ + \ configuration: 'run-to-breakpoint' + \ } } ) + + " Make sure the Test t is initialised + call vimspector#StepOver() + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( fn, 26, 1 ) + + call win_gotoid( g:vimspector_session_windows.watches ) + call feedkeys( "it\", 'xt' ) + + call WaitForAssert( {-> + \ assert_equal( + \ [ + \ 'Watches: ----', + \ 'Expression: t', + \ ' *+ Result: {...}', + \ ], + \ getbufline( winbufnr( g:vimspector_session_windows.watches ), + \ 1, + \ '$' ) + \ ) + \ } ) + call assert_equal( 'cpp', + \ getbufvar( + \ winbufnr( g:vimspector_session_windows.watches ), + \ '&syntax' ) ) + + " Expand + call win_gotoid( g:vimspector_session_windows.watches ) + call setpos( '.', [ 0, 3, 1 ] ) + call feedkeys( "\", 'xt' ) + + call WaitForAssert( {-> + \ AssertMatchList( + \ [ + \ 'Watches: ----', + \ 'Expression: t', + \ ' \*- Result: {...}', + \ ' \*- i (int): 0', + \ ' \*- c (char): 0 ''\\0\{1,3}''', + \ ' \*- fffff (float): 0', + \ ' \*+ another_test (AnotherTest):\( {...}\)\?', + \ ], + \ getbufline( winbufnr( g:vimspector_session_windows.watches ), + \ 1, + \ '$' ) + \ ) + \ } ) + + call setpos( '.', [ 0, 4, 1 ] ) + + " We can't just fire the keys to the inpit prompt because we use inputsave() + " and inputrestore(), so mock that out and fire away. + " Note: mapleder is , + py3 <\100\", "xt" )' ) +EOF + + + call WaitForAssert( {-> + \ AssertMatchList( + \ [ + \ 'Watches: ----', + \ 'Expression: t', + \ ' \*- Result: {...}', + \ ' \*- i (int): 100', + \ ' \*- c (char): 0 ''\\0\{1,3}''', + \ ' \*- fffff (float): 0', + \ ' \*+ another_test (AnotherTest):\( {...}\)\?', + \ ], + \ getbufline( winbufnr( g:vimspector_session_windows.watches ), + \ 1, + \ '$' ) + \ ) + \ } ) + + " Now set it via the more comforable scripting interface + call vimspector#SetVariableValue( '1234' ) + + call WaitForAssert( {-> + \ AssertMatchList( + \ [ + \ 'Watches: ----', + \ 'Expression: t', + \ ' \*- Result: {...}', + \ ' \*- i (int): 1234', + \ ' \*- c (char): 0 ''\\0\{1,3}''', + \ ' \*- fffff (float): 0', + \ ' \*+ another_test (AnotherTest):\( {...}\)\?', + \ ], + \ getbufline( winbufnr( g:vimspector_session_windows.watches ), + \ 1, + \ '$' ) + \ ) + \ } ) + + call vimspector#test#setup#Reset() + %bwipe! +endfunction + +function! Test_SetVariableValue_Balloon() + let fn = 'testdata/cpp/simple/struct.cpp' + call s:StartDebugging( #{ fn: fn, line: 24, col: 1, launch: #{ + \ configuration: 'run-to-breakpoint' + \ } } ) + + call vimspector#StepOver() + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( fn, 26, 1 ) + + " leader is , + xmap d VimspectorBalloonEval + nmap d VimspectorBalloonEval + + "evaluate the prev line + call setpos( '.', [ 0, 24, 8 ] ) + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( fn, 24, 8 ) + call feedkeys( ',d', 'xt' ) + + call WaitForAssert( {-> + \ assert_notequal( v:none, g:vimspector_session_windows.eval ) + \ } ) + + call WaitForAssert( {-> + \ AssertMatchList( + \ [ + \ '{...}', + \ ' - i: 0', + \ ' - c: 0 ''\\0\{1,3}''', + \ ' - fffff: 0', + \ ' + another_test: ', + \ ], + \ getbufline( winbufnr( g:vimspector_session_windows.eval ), + \ 1, + \ '$' ) + \ ) + \ } ) + + " Move down to the ffff line + + call feedkeys( 'jjj', 'xt' ) + " We can't just fire the keys to the inpit prompt because we use inputsave() + " and inputrestore(), so mock that out and fire away. + " Note: mapleder is , + py3 <\100\", "xt" )' ) +EOF + + call WaitForAssert( {-> + \ AssertMatchList( + \ [ + \ '{...}', + \ ' - i: 0', + \ ' - c: 0 ''\\0\{1,3}''', + \ ' - fffff: 100', + \ ' + another_test: ', + \ ], + \ getbufline( winbufnr( g:vimspector_session_windows.eval ), + \ 1, + \ '$' ) + \ ) + \ } ) + + call vimspector#test#setup#Reset() + %bwipe! +endfunction diff --git a/tests/vimrc b/tests/vimrc index e845f78..c65efa8 100644 --- a/tests/vimrc +++ b/tests/vimrc @@ -1,7 +1,10 @@ let g:vimspector_test_plugin_path = expand( ':p:h:h' ) set mouse=a +set noequalalways +let mapleader = ',' +let maplocalleader = "\" -let &rtp = &rtp . ',' . g:vimspector_test_plugin_path +let &runtimepath = &runtimepath . ',' . g:vimspector_test_plugin_path filetype plugin indent on syntax enable diff --git a/tox.ini b/tox.ini index dbd9626..7a6c06e 100644 --- a/tox.ini +++ b/tox.ini @@ -1,3 +1,4 @@ [flake8] ignore = E111,E114,E121,E125,E126,E127,E128,E129,E131,E133,E201,E202,E203,E211,E221,E222,E241,E251,E261,E303,E402,W503,W504 max-line-length = 80 +exclude = python3/vimspector/vendor