├── .gitattributes ├── .github ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ └── pr_build.yaml ├── .gitignore ├── LICENSE ├── README.md ├── RestClientVS.sln ├── art ├── context-menu.png ├── document.png ├── options.png ├── screenshot.png └── tooltip.png ├── nuget.config ├── src ├── RestClient │ ├── Client │ │ ├── HttpClientExtensions.cs │ │ ├── RequestResult.cs │ │ └── RequestSender.cs │ ├── Constants.cs │ ├── Parser │ │ ├── Document.cs │ │ ├── DocumentParser.cs │ │ ├── Header.cs │ │ ├── ItemType.cs │ │ ├── ParseItem.cs │ │ ├── Request.cs │ │ ├── StringExtensions.cs │ │ └── Variable.cs │ └── RestClient.csproj └── RestClientVS │ ├── Commands │ ├── CommentCommand.cs │ ├── Commenting.cs │ ├── GoToDefinitionCommand.cs │ ├── SendRequestCommand.cs │ └── UncommentCommand.cs │ ├── Editor │ ├── DropdownBars.cs │ ├── EditorFeatures.cs │ ├── IntelliSense.cs │ ├── IntelliSenseCatalog.cs │ ├── LanguageFactory.cs │ ├── PlayButtonAdornment.cs │ └── TokenTagger.cs │ ├── ExtensionMethods.cs │ ├── Margin │ ├── ResponseControl.xaml │ ├── ResponseControl.xaml.cs │ ├── ResponseMargin.cs │ └── ResponseMarginProvider.cs │ ├── Options │ └── General.cs │ ├── OutputWindow │ ├── ClassificationTypeDefinitions.cs │ └── OutputClassifier.cs │ ├── Properties │ └── AssemblyInfo.cs │ ├── Resources │ └── Icon.png │ ├── RestClientVS.csproj │ ├── RestClientVSPackage.cs │ ├── RestDocument.cs │ ├── VSCommandTable.cs │ ├── VSCommandTable.vsct │ ├── source.extension.cs │ └── source.extension.vsixmanifest ├── test └── RestClientTest │ ├── Extensions.cs │ ├── HttpTest.cs │ ├── RestClientTest.csproj │ ├── TokenTest.cs │ └── VariableTest.cs └── vs-publish.json /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | @mkristensen on Twitter. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Looking to contribute something? **Here's how you can help.** 4 | 5 | Please take a moment to review this document in order to make the contribution 6 | process easy and effective for everyone involved. 7 | 8 | Following these guidelines helps to communicate that you respect the time of 9 | the developers managing and developing this open source project. In return, 10 | they should reciprocate that respect in addressing your issue or assessing 11 | patches and features. 12 | 13 | 14 | ## Using the issue tracker 15 | 16 | The issue tracker is the preferred channel for [bug reports](#bug-reports), 17 | [features requests](#feature-requests) and 18 | [submitting pull requests](#pull-requests), but please respect the 19 | following restrictions: 20 | 21 | * Please **do not** use the issue tracker for personal support requests. Stack 22 | Overflow is a better place to get help. 23 | 24 | * Please **do not** derail or troll issues. Keep the discussion on topic and 25 | respect the opinions of others. 26 | 27 | * Please **do not** open issues or pull requests which *belongs to* third party 28 | components. 29 | 30 | 31 | ## Bug reports 32 | 33 | A bug is a _demonstrable problem_ that is caused by the code in the repository. 34 | Good bug reports are extremely helpful, so thanks! 35 | 36 | Guidelines for bug reports: 37 | 38 | 1. **Use the GitHub issue search** — check if the issue has already been 39 | reported. 40 | 41 | 2. **Check if the issue has been fixed** — try to reproduce it using the 42 | latest `master` or development branch in the repository. 43 | 44 | 3. **Isolate the problem** — ideally create an 45 | [SSCCE](http://www.sscce.org/) and a live example. 46 | Uploading the project on cloud storage (OneDrive, DropBox, et el.) 47 | or creating a sample GitHub repository is also helpful. 48 | 49 | 50 | A good bug report shouldn't leave others needing to chase you up for more 51 | information. Please try to be as detailed as possible in your report. What is 52 | your environment? What steps will reproduce the issue? What browser(s) and OS 53 | experience the problem? Do other browsers show the bug differently? What 54 | would you expect to be the outcome? All these details will help people to fix 55 | any potential bugs. 56 | 57 | Example: 58 | 59 | > Short and descriptive example bug report title 60 | > 61 | > A summary of the issue and the Visual Studio, browser, OS environments 62 | > in which it occurs. If suitable, include the steps required to reproduce the bug. 63 | > 64 | > 1. This is the first step 65 | > 2. This is the second step 66 | > 3. Further steps, etc. 67 | > 68 | > `` - a link to the project/file uploaded on cloud storage or other publicly accessible medium. 69 | > 70 | > Any other information you want to share that is relevant to the issue being 71 | > reported. This might include the lines of code that you have identified as 72 | > causing the bug, and potential solutions (and your opinions on their 73 | > merits). 74 | 75 | 76 | ## Feature requests 77 | 78 | Feature requests are welcome. But take a moment to find out whether your idea 79 | fits with the scope and aims of the project. It's up to *you* to make a strong 80 | case to convince the project's developers of the merits of this feature. Please 81 | provide as much detail and context as possible. 82 | 83 | 84 | ## Pull requests 85 | 86 | Good pull requests, patches, improvements and new features are a fantastic 87 | help. They should remain focused in scope and avoid containing unrelated 88 | commits. 89 | 90 | **Please ask first** before embarking on any significant pull request (e.g. 91 | implementing features, refactoring code, porting to a different language), 92 | otherwise you risk spending a lot of time working on something that the 93 | project's developers might not want to merge into the project. 94 | 95 | Please adhere to the [coding guidelines](#code-guidelines) used throughout the 96 | project (indentation, accurate comments, etc.) and any other requirements 97 | (such as test coverage). 98 | 99 | Adhering to the following process is the best way to get your work 100 | included in the project: 101 | 102 | 1. [Fork](http://help.github.com/fork-a-repo/) the project, clone your fork, 103 | and configure the remotes: 104 | 105 | ```bash 106 | # Clone your fork of the repo into the current directory 107 | git clone https://github.com//.git 108 | # Navigate to the newly cloned directory 109 | cd 110 | # Assign the original repo to a remote called "upstream" 111 | git remote add upstream https://github.com/madskristensen/.git 112 | ``` 113 | 114 | 2. If you cloned a while ago, get the latest changes from upstream: 115 | 116 | ```bash 117 | git checkout master 118 | git pull upstream master 119 | ``` 120 | 121 | 3. Create a new topic branch (off the main project development branch) to 122 | contain your feature, change, or fix: 123 | 124 | ```bash 125 | git checkout -b 126 | ``` 127 | 128 | 4. Commit your changes in logical chunks. Please adhere to these [git commit 129 | message guidelines](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html) 130 | or your code is unlikely be merged into the main project. Use Git's 131 | [interactive rebase](https://help.github.com/articles/interactive-rebase) 132 | feature to tidy up your commits before making them public. Also, prepend name of the feature 133 | to the commit message. For instance: "SCSS: Fixes compiler results for IFileListener.\nFixes `#123`" 134 | 135 | 5. Locally merge (or rebase) the upstream development branch into your topic branch: 136 | 137 | ```bash 138 | git pull [--rebase] upstream master 139 | ``` 140 | 141 | 6. Push your topic branch up to your fork: 142 | 143 | ```bash 144 | git push origin 145 | ``` 146 | 147 | 7. [Open a Pull Request](https://help.github.com/articles/using-pull-requests/) 148 | with a clear title and description against the `master` branch. 149 | 150 | 151 | ## Code guidelines 152 | 153 | - Always use proper indentation. 154 | - In Visual Studio under `Tools > Options > Text Editor > C# > Advanced`, make sure 155 | `Place 'System' directives first when sorting usings` option is enabled (checked). 156 | - Before committing, organize usings for each updated C# source file. Either you can 157 | right-click editor and select `Organize Usings > Remove and sort` OR use extension 158 | like [BatchFormat](https://marketplace.visualstudio.com/items?itemName=vs-publisher-147549.BatchFormat). 159 | - Before committing, run Code Analysis in `Debug` configuration and follow the guidelines 160 | to fix CA issues. Code Analysis commits can be made separately. 161 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: madskristensen 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Additional context** 27 | Add any other context about the problem here. 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/pr_build.yaml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json 2 | name: "Build" 3 | 4 | on: 5 | push: 6 | branches: [master] 7 | pull_request: 8 | branches: [master] 9 | workflow_dispatch: 10 | branches: [master] 11 | 12 | jobs: 13 | build: 14 | outputs: 15 | version: ${{ steps.vsix_version.outputs.version-number }} 16 | name: Build 17 | runs-on: windows-2022 18 | env: 19 | DOTNET_CLI_TELEMETRY_OPTOUT: 1 20 | DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1 21 | DOTNET_NOLOGO: true 22 | DOTNET_GENERATE_ASPNET_CERTIFICATE: false 23 | DOTNET_ADD_GLOBAL_TOOLS_TO_PATH: false 24 | DOTNET_MULTILEVEL_LOOKUP: 0 25 | 26 | steps: 27 | - uses: actions/checkout@v2 28 | 29 | - name: Setup .NET build dependencies 30 | uses: timheuer/bootstrap-dotnet@v1 31 | with: 32 | nuget: 'false' 33 | 34 | - name: Increment VSIX version 35 | id: vsix_version 36 | uses: timheuer/vsix-version-stamp@v1 37 | with: 38 | manifest-file: src\RestClientVS\source.extension.vsixmanifest 39 | vsix-token-source-file: src\RestClientVS\source.extension.cs 40 | 41 | - name: Build 42 | run: msbuild /p:Configuration=Release /p:DeployExtension=false /p:ZipPackageCompressionLevel=normal /v:m -restore /p:OutDir=../../built 43 | 44 | - name: Test 45 | uses: zyborg/dotnet-tests-report@v1 46 | with: 47 | project_path: test\RestClientTest\RestClientTest.csproj 48 | report_name: restclient_tests 49 | report_title: RestClient Tests 50 | github_token: ${{ secrets.GITHUB_TOKEN }} 51 | set_check_status_from_test_outcome: true 52 | 53 | - name: Upload artifact 54 | uses: actions/upload-artifact@v2 55 | with: 56 | name: ${{ github.event.repository.name }}.vsix 57 | path: built/**/*.vsix 58 | 59 | publish: 60 | if: ${{ github.event_name == 'push' || github.event_name == 'workflow_dispatch' }} 61 | needs: build 62 | runs-on: windows-latest 63 | steps: 64 | 65 | - uses: actions/checkout@v2 66 | 67 | - name: Download Package artifact 68 | uses: actions/download-artifact@v2 69 | with: 70 | name: ${{ github.event.repository.name }}.vsix 71 | 72 | - name: Upload to Open VSIX 73 | uses: timheuer/openvsixpublish@v1 74 | with: 75 | vsix-file: ${{ github.event.repository.name }}.vsix 76 | 77 | - name: Tag and Release 78 | if: ${{ github.event_name == 'workflow_dispatch' || contains(github.event.head_commit.message, '[release]') }} 79 | id: tag_release 80 | uses: softprops/action-gh-release@v0.1.13 81 | with: 82 | body: Release ${{ needs.build.outputs.version }} 83 | tag_name: ${{ needs.build.outputs.version }} 84 | files: | 85 | **/*.vsix\ 86 | 87 | - name: Publish extension to Marketplace 88 | if: ${{ github.event_name == 'workflow_dispatch' || contains(github.event.head_commit.message, '[release]') }} 89 | uses: cezarypiatek/VsixPublisherAction@0.1 90 | with: 91 | extension-file: '${{ github.event.repository.name }}.vsix' 92 | publish-manifest-file: 'vs-publish.json' 93 | personal-access-code: ${{ secrets.VS_PUBLISHER_ACCESS_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Ww][Ii][Nn]32/ 27 | [Aa][Rr][Mm]/ 28 | [Aa][Rr][Mm]64/ 29 | bld/ 30 | [Bb]in/ 31 | [Oo]bj/ 32 | [Oo]ut/ 33 | [Ll]og/ 34 | [Ll]ogs/ 35 | 36 | # Visual Studio 2015/2017 cache/options directory 37 | .vs/ 38 | # Uncomment if you have tasks that create the project's static files in wwwroot 39 | #wwwroot/ 40 | 41 | # Visual Studio 2017 auto generated files 42 | Generated\ Files/ 43 | 44 | # MSTest test Results 45 | [Tt]est[Rr]esult*/ 46 | [Bb]uild[Ll]og.* 47 | 48 | # NUnit 49 | *.VisualState.xml 50 | TestResult.xml 51 | nunit-*.xml 52 | 53 | # Build Results of an ATL Project 54 | [Dd]ebugPS/ 55 | [Rr]eleasePS/ 56 | dlldata.c 57 | 58 | # Benchmark Results 59 | BenchmarkDotNet.Artifacts/ 60 | 61 | # .NET Core 62 | project.lock.json 63 | project.fragment.lock.json 64 | artifacts/ 65 | 66 | # ASP.NET Scaffolding 67 | ScaffoldingReadMe.txt 68 | 69 | # StyleCop 70 | StyleCopReport.xml 71 | 72 | # Files built by Visual Studio 73 | *_i.c 74 | *_p.c 75 | *_h.h 76 | *.ilk 77 | *.meta 78 | *.obj 79 | *.iobj 80 | *.pch 81 | *.pdb 82 | *.ipdb 83 | *.pgc 84 | *.pgd 85 | *.rsp 86 | *.sbr 87 | *.tlb 88 | *.tli 89 | *.tlh 90 | *.tmp 91 | *.tmp_proj 92 | *_wpftmp.csproj 93 | *.log 94 | *.vspscc 95 | *.vssscc 96 | .builds 97 | *.pidb 98 | *.svclog 99 | *.scc 100 | 101 | # Chutzpah Test files 102 | _Chutzpah* 103 | 104 | # Visual C++ cache files 105 | ipch/ 106 | *.aps 107 | *.ncb 108 | *.opendb 109 | *.opensdf 110 | *.sdf 111 | *.cachefile 112 | *.VC.db 113 | *.VC.VC.opendb 114 | 115 | # Visual Studio profiler 116 | *.psess 117 | *.vsp 118 | *.vspx 119 | *.sap 120 | 121 | # Visual Studio Trace Files 122 | *.e2e 123 | 124 | # TFS 2012 Local Workspace 125 | $tf/ 126 | 127 | # Guidance Automation Toolkit 128 | *.gpState 129 | 130 | # ReSharper is a .NET coding add-in 131 | _ReSharper*/ 132 | *.[Rr]e[Ss]harper 133 | *.DotSettings.user 134 | 135 | # TeamCity is a build add-in 136 | _TeamCity* 137 | 138 | # DotCover is a Code Coverage Tool 139 | *.dotCover 140 | 141 | # AxoCover is a Code Coverage Tool 142 | .axoCover/* 143 | !.axoCover/settings.json 144 | 145 | # Coverlet is a free, cross platform Code Coverage Tool 146 | coverage*.json 147 | coverage*.xml 148 | coverage*.info 149 | 150 | # Visual Studio code coverage results 151 | *.coverage 152 | *.coveragexml 153 | 154 | # NCrunch 155 | _NCrunch_* 156 | .*crunch*.local.xml 157 | nCrunchTemp_* 158 | 159 | # MightyMoose 160 | *.mm.* 161 | AutoTest.Net/ 162 | 163 | # Web workbench (sass) 164 | .sass-cache/ 165 | 166 | # Installshield output folder 167 | [Ee]xpress/ 168 | 169 | # DocProject is a documentation generator add-in 170 | DocProject/buildhelp/ 171 | DocProject/Help/*.HxT 172 | DocProject/Help/*.HxC 173 | DocProject/Help/*.hhc 174 | DocProject/Help/*.hhk 175 | DocProject/Help/*.hhp 176 | DocProject/Help/Html2 177 | DocProject/Help/html 178 | 179 | # Click-Once directory 180 | publish/ 181 | 182 | # Publish Web Output 183 | *.[Pp]ublish.xml 184 | *.azurePubxml 185 | # Note: Comment the next line if you want to checkin your web deploy settings, 186 | # but database connection strings (with potential passwords) will be unencrypted 187 | *.pubxml 188 | *.publishproj 189 | 190 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 191 | # checkin your Azure Web App publish settings, but sensitive information contained 192 | # in these scripts will be unencrypted 193 | PublishScripts/ 194 | 195 | # NuGet Packages 196 | *.nupkg 197 | # NuGet Symbol Packages 198 | *.snupkg 199 | # The packages folder can be ignored because of Package Restore 200 | **/[Pp]ackages/* 201 | # except build/, which is used as an MSBuild target. 202 | !**/[Pp]ackages/build/ 203 | # Uncomment if necessary however generally it will be regenerated when needed 204 | #!**/[Pp]ackages/repositories.config 205 | # NuGet v3's project.json files produces more ignorable files 206 | *.nuget.props 207 | *.nuget.targets 208 | 209 | # Microsoft Azure Build Output 210 | csx/ 211 | *.build.csdef 212 | 213 | # Microsoft Azure Emulator 214 | ecf/ 215 | rcf/ 216 | 217 | # Windows Store app package directories and files 218 | AppPackages/ 219 | BundleArtifacts/ 220 | Package.StoreAssociation.xml 221 | _pkginfo.txt 222 | *.appx 223 | *.appxbundle 224 | *.appxupload 225 | 226 | # Visual Studio cache files 227 | # files ending in .cache can be ignored 228 | *.[Cc]ache 229 | # but keep track of directories ending in .cache 230 | !?*.[Cc]ache/ 231 | 232 | # Others 233 | ClientBin/ 234 | ~$* 235 | *~ 236 | *.dbmdl 237 | *.dbproj.schemaview 238 | *.jfm 239 | *.pfx 240 | *.publishsettings 241 | orleans.codegen.cs 242 | 243 | # Including strong name files can present a security risk 244 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 245 | #*.snk 246 | 247 | # Since there are multiple workflows, uncomment next line to ignore bower_components 248 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 249 | #bower_components/ 250 | 251 | # RIA/Silverlight projects 252 | Generated_Code/ 253 | 254 | # Backup & report files from converting an old project file 255 | # to a newer Visual Studio version. Backup files are not needed, 256 | # because we have git ;-) 257 | _UpgradeReport_Files/ 258 | Backup*/ 259 | UpgradeLog*.XML 260 | UpgradeLog*.htm 261 | ServiceFabricBackup/ 262 | *.rptproj.bak 263 | 264 | # SQL Server files 265 | *.mdf 266 | *.ldf 267 | *.ndf 268 | 269 | # Business Intelligence projects 270 | *.rdl.data 271 | *.bim.layout 272 | *.bim_*.settings 273 | *.rptproj.rsuser 274 | *- [Bb]ackup.rdl 275 | *- [Bb]ackup ([0-9]).rdl 276 | *- [Bb]ackup ([0-9][0-9]).rdl 277 | 278 | # Microsoft Fakes 279 | FakesAssemblies/ 280 | 281 | # GhostDoc plugin setting file 282 | *.GhostDoc.xml 283 | 284 | # Node.js Tools for Visual Studio 285 | .ntvs_analysis.dat 286 | node_modules/ 287 | 288 | # Visual Studio 6 build log 289 | *.plg 290 | 291 | # Visual Studio 6 workspace options file 292 | *.opt 293 | 294 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 295 | *.vbw 296 | 297 | # Visual Studio LightSwitch build output 298 | **/*.HTMLClient/GeneratedArtifacts 299 | **/*.DesktopClient/GeneratedArtifacts 300 | **/*.DesktopClient/ModelManifest.xml 301 | **/*.Server/GeneratedArtifacts 302 | **/*.Server/ModelManifest.xml 303 | _Pvt_Extensions 304 | 305 | # Paket dependency manager 306 | .paket/paket.exe 307 | paket-files/ 308 | 309 | # FAKE - F# Make 310 | .fake/ 311 | 312 | # CodeRush personal settings 313 | .cr/personal 314 | 315 | # Python Tools for Visual Studio (PTVS) 316 | __pycache__/ 317 | *.pyc 318 | 319 | # Cake - Uncomment if you are using it 320 | # tools/** 321 | # !tools/packages.config 322 | 323 | # Tabs Studio 324 | *.tss 325 | 326 | # Telerik's JustMock configuration file 327 | *.jmconfig 328 | 329 | # BizTalk build output 330 | *.btp.cs 331 | *.btm.cs 332 | *.odx.cs 333 | *.xsd.cs 334 | 335 | # OpenCover UI analysis results 336 | OpenCover/ 337 | 338 | # Azure Stream Analytics local run output 339 | ASALocalRun/ 340 | 341 | # MSBuild Binary and Structured Log 342 | *.binlog 343 | 344 | # NVidia Nsight GPU debugger configuration file 345 | *.nvuser 346 | 347 | # MFractors (Xamarin productivity tool) working folder 348 | .mfractor/ 349 | 350 | # Local History for Visual Studio 351 | .localhistory/ 352 | 353 | # BeatPulse healthcheck temp database 354 | healthchecksdb 355 | 356 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 357 | MigrationBackup/ 358 | 359 | # Ionide (cross platform F# VS Code tools) working folder 360 | .ionide/ 361 | 362 | # Fody - auto-generated XML schema 363 | FodyWeavers.xsd -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2015 Mads Kristensen 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [marketplace]: https://marketplace.visualstudio.com/items?itemName=MadsKristensen.RestClient 2 | [vsixgallery]: http://vsixgallery.com/extension/RestClientVS.a7b4a362-3ce8-4953-9b19-a35166f2cbfd 3 | [repo]:https://github.com/madskristensen/RestClientVS/ 4 | 5 | # Rest Client for Visual Studio 6 | 7 | **IMPORTANT:** This extension can only be installed in Visual Studio 2022 8 | up until version 17.4. In version 17.5 support for .http files were built 9 | in to Visual Studio. 10 | 11 | **Download** this extension from the [VS Marketplace][marketplace] 12 | or get the [CI build](https://www.vsixgallery.com/extension/RestClientVS.a7b4a362-3ce8-4953-9b19-a35166f2cbfd). 13 | 14 | ----------------------------------- 15 | 16 | REST Client allows you to send HTTP request and view the response in Visual Studio directly. Based on the popular VS Code extension [Rest Client](https://marketplace.visualstudio.com/items?itemName=humao.rest-client) by [Huachao Mao](https://github.com/Huachao) 17 | 18 | ![Screenshot](art/screenshot.png) 19 | 20 | ## The .http file 21 | Any files with the extension `.http` or `.rest` is the entry point to creating HTTP requests. 22 | 23 | Here's an example of how to define the requests with variables and code comments. 24 | 25 | ```css 26 | @hostname = localhost 27 | @port = 8080 28 | @host = {{hostname}}:{{port}} 29 | @contentType = application/json 30 | 31 | POST https://{{host}}/authors/create 32 | Content-Type:{{contentType}} 33 | 34 | { 35 | "name": "Joe", 36 | "permissions": "author" 37 | } 38 | 39 | ### 40 | 41 | # Comments 42 | GET https://{{host}}/authors/ 43 | 44 | ### 45 | 46 | GET https://www.bing.com 47 | ``` 48 | 49 | This is what it looks like in the Blue Theme. 50 | 51 | ![Document](art/document.png) 52 | 53 | Notice the green play buttons showing up after each URL. Clicking those will fire off the request and display the raw response on the right side of the document. 54 | 55 | ![Tooltip](art/tooltip.png) 56 | 57 | You can also right-click to find the Send Request command or use the **Ctrl+Alt+S** keyboard shortcut. 58 | 59 | ![Context Menu](art/context-menu.png) 60 | 61 | You can set the timeout of the HTTP requests from the *Options* dialog. 62 | 63 | ![Options](art/options.png) 64 | 65 | ### How can I help? 66 | If you enjoy using the extension, please give it a ★★★★★ rating on the [Visual Studio Marketplace][marketplace]. 67 | 68 | Should you encounter bugs or if you have feature requests, head on over to the [GitHub repo][repo] to open an issue if one doesn't already exist. 69 | 70 | Pull requests are also very welcome, since I can't always get around to fixing all bugs myself. This is a personal passion project, so my time is limited. 71 | 72 | Another way to help out is to [sponser me on GitHub](https://github.com/sponsors/madskristensen). -------------------------------------------------------------------------------- /RestClientVS.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.1.32005.40 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RestClientVS", "src\RestClientVS\RestClientVS.csproj", "{3EFB5569-5699-4010-9173-27ED12EB6245}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RestClient", "src\RestClient\RestClient.csproj", "{2479064B-68E0-430E-BFEB-E2ECC670E019}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RestClientTest", "test\RestClientTest\RestClientTest.csproj", "{EF880336-6A97-4D62-B860-22D264A3F719}" 11 | EndProject 12 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{4120CAD3-28E5-49E9-8398-A82F4F86BAB8}" 13 | EndProject 14 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{99EE9F4E-D9B3-4EB4-ABE8-8BE1E38FFAAE}" 15 | EndProject 16 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{88B160D5-D161-408D-9CDB-D77AF3F3B14E}" 17 | ProjectSection(SolutionItems) = preProject 18 | vs-publish.json = vs-publish.json 19 | .github\workflows\pr_build.yaml = .github\workflows\pr_build.yaml 20 | README.md = README.md 21 | EndProjectSection 22 | EndProject 23 | Global 24 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 25 | Debug|Any CPU = Debug|Any CPU 26 | Debug|x86 = Debug|x86 27 | Release|Any CPU = Release|Any CPU 28 | Release|x86 = Release|x86 29 | EndGlobalSection 30 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 31 | {3EFB5569-5699-4010-9173-27ED12EB6245}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 32 | {3EFB5569-5699-4010-9173-27ED12EB6245}.Debug|Any CPU.Build.0 = Debug|Any CPU 33 | {3EFB5569-5699-4010-9173-27ED12EB6245}.Debug|x86.ActiveCfg = Debug|x86 34 | {3EFB5569-5699-4010-9173-27ED12EB6245}.Debug|x86.Build.0 = Debug|x86 35 | {3EFB5569-5699-4010-9173-27ED12EB6245}.Release|Any CPU.ActiveCfg = Release|Any CPU 36 | {3EFB5569-5699-4010-9173-27ED12EB6245}.Release|Any CPU.Build.0 = Release|Any CPU 37 | {3EFB5569-5699-4010-9173-27ED12EB6245}.Release|x86.ActiveCfg = Release|x86 38 | {3EFB5569-5699-4010-9173-27ED12EB6245}.Release|x86.Build.0 = Release|x86 39 | {2479064B-68E0-430E-BFEB-E2ECC670E019}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 40 | {2479064B-68E0-430E-BFEB-E2ECC670E019}.Debug|Any CPU.Build.0 = Debug|Any CPU 41 | {2479064B-68E0-430E-BFEB-E2ECC670E019}.Debug|x86.ActiveCfg = Debug|Any CPU 42 | {2479064B-68E0-430E-BFEB-E2ECC670E019}.Debug|x86.Build.0 = Debug|Any CPU 43 | {2479064B-68E0-430E-BFEB-E2ECC670E019}.Release|Any CPU.ActiveCfg = Release|Any CPU 44 | {2479064B-68E0-430E-BFEB-E2ECC670E019}.Release|Any CPU.Build.0 = Release|Any CPU 45 | {2479064B-68E0-430E-BFEB-E2ECC670E019}.Release|x86.ActiveCfg = Release|Any CPU 46 | {2479064B-68E0-430E-BFEB-E2ECC670E019}.Release|x86.Build.0 = Release|Any CPU 47 | {EF880336-6A97-4D62-B860-22D264A3F719}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 48 | {EF880336-6A97-4D62-B860-22D264A3F719}.Debug|Any CPU.Build.0 = Debug|Any CPU 49 | {EF880336-6A97-4D62-B860-22D264A3F719}.Debug|x86.ActiveCfg = Debug|Any CPU 50 | {EF880336-6A97-4D62-B860-22D264A3F719}.Debug|x86.Build.0 = Debug|Any CPU 51 | {EF880336-6A97-4D62-B860-22D264A3F719}.Release|Any CPU.ActiveCfg = Release|Any CPU 52 | {EF880336-6A97-4D62-B860-22D264A3F719}.Release|Any CPU.Build.0 = Release|Any CPU 53 | {EF880336-6A97-4D62-B860-22D264A3F719}.Release|x86.ActiveCfg = Release|Any CPU 54 | {EF880336-6A97-4D62-B860-22D264A3F719}.Release|x86.Build.0 = Release|Any CPU 55 | EndGlobalSection 56 | GlobalSection(SolutionProperties) = preSolution 57 | HideSolutionNode = FALSE 58 | EndGlobalSection 59 | GlobalSection(NestedProjects) = preSolution 60 | {3EFB5569-5699-4010-9173-27ED12EB6245} = {99EE9F4E-D9B3-4EB4-ABE8-8BE1E38FFAAE} 61 | {2479064B-68E0-430E-BFEB-E2ECC670E019} = {99EE9F4E-D9B3-4EB4-ABE8-8BE1E38FFAAE} 62 | {EF880336-6A97-4D62-B860-22D264A3F719} = {4120CAD3-28E5-49E9-8398-A82F4F86BAB8} 63 | EndGlobalSection 64 | GlobalSection(ExtensibilityGlobals) = postSolution 65 | SolutionGuid = {AD821659-B39C-4BF8-8B13-F612C312C3DD} 66 | EndGlobalSection 67 | EndGlobal 68 | -------------------------------------------------------------------------------- /art/context-menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madskristensen/RestClientVS/ba6bdfc9a74011249d7713fcff72aa796b22450e/art/context-menu.png -------------------------------------------------------------------------------- /art/document.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madskristensen/RestClientVS/ba6bdfc9a74011249d7713fcff72aa796b22450e/art/document.png -------------------------------------------------------------------------------- /art/options.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madskristensen/RestClientVS/ba6bdfc9a74011249d7713fcff72aa796b22450e/art/options.png -------------------------------------------------------------------------------- /art/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madskristensen/RestClientVS/ba6bdfc9a74011249d7713fcff72aa796b22450e/art/screenshot.png -------------------------------------------------------------------------------- /art/tooltip.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madskristensen/RestClientVS/ba6bdfc9a74011249d7713fcff72aa796b22450e/art/tooltip.png -------------------------------------------------------------------------------- /nuget.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/RestClient/Client/HttpClientExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Net.Http; 3 | using System.Text; 4 | using System.Threading.Tasks; 5 | 6 | namespace RestClient.Client 7 | { 8 | // Found on: https://www.jordanbrown.dev/2021/02/06/2021/http-to-raw-string-csharp/ 9 | public static class HttpClientExtensions 10 | { 11 | public static async Task ToRawStringAsync(this HttpRequestMessage request) 12 | { 13 | var sb = new StringBuilder(); 14 | 15 | var line1 = $"{request.Method} {request.RequestUri} HTTP/{request.Version}"; 16 | sb.AppendLine(line1); 17 | 18 | foreach (KeyValuePair> instance in request.Headers) 19 | { 20 | foreach (var val in instance.Value) 21 | { 22 | var header = $"{instance.Key}: {val}"; 23 | sb.AppendLine(header); 24 | } 25 | } 26 | 27 | if (request.Content?.Headers != null) 28 | { 29 | foreach (KeyValuePair> instance in request.Content.Headers) 30 | { 31 | foreach (var val in instance.Value) 32 | { 33 | var header = $"{instance.Key}: {val}"; 34 | sb.AppendLine(header); 35 | } 36 | } 37 | } 38 | sb.AppendLine(); 39 | 40 | var body = await (request.Content?.ReadAsStringAsync() ?? Task.FromResult(null)); 41 | if (!string.IsNullOrWhiteSpace(body)) 42 | { 43 | sb.AppendLine(body); 44 | } 45 | 46 | return sb.ToString(); 47 | } 48 | 49 | public static async Task ToRawStringAsync(this HttpResponseMessage response) 50 | { 51 | var sb = new StringBuilder(); 52 | 53 | var statusCode = (int)response.StatusCode; 54 | var line1 = $"HTTP/{response.Version} {statusCode} {response.ReasonPhrase}"; 55 | sb.AppendLine(line1); 56 | sb.AppendLine(); 57 | 58 | foreach (KeyValuePair> keyValuePair in response.Headers) 59 | { 60 | foreach (var val in keyValuePair.Value) 61 | { 62 | var header = $"{keyValuePair.Key}: {val}"; 63 | sb.AppendLine(header); 64 | } 65 | } 66 | 67 | foreach (KeyValuePair> keyValuePair in response.Content.Headers) 68 | { 69 | foreach (var val in keyValuePair.Value) 70 | { 71 | var header = $"{keyValuePair.Key}: {val}"; 72 | sb.AppendLine(header); 73 | } 74 | } 75 | 76 | sb.AppendLine(); 77 | 78 | var body = await response.Content.ReadAsStringAsync(); 79 | if (!string.IsNullOrWhiteSpace(body)) 80 | { 81 | sb.AppendLine(body); 82 | } 83 | 84 | return sb.ToString(); 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/RestClient/Client/RequestResult.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http; 2 | 3 | namespace RestClient.Client 4 | { 5 | public class RequestResult 6 | { 7 | public HttpRequestMessage? Request { get; internal set; } 8 | public HttpResponseMessage? Response { get; internal set; } 9 | public string? ErrorMessage { get; internal set; } 10 | public Request? RequestToken { get; internal set; } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/RestClient/Client/RequestSender.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Http; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | 6 | namespace RestClient.Client 7 | { 8 | public static class RequestSender 9 | { 10 | 11 | 12 | public static async Task SendAsync(Request request, TimeSpan timeOut, CancellationToken cancellationToken = default) 13 | { 14 | RequestResult result = new() { RequestToken = request }; 15 | HttpRequestMessage? requestMessage = BuildRequest(request, result); 16 | 17 | var handler = new HttpClientHandler 18 | { 19 | AllowAutoRedirect = true, 20 | UseDefaultCredentials = true, 21 | }; 22 | 23 | using (var client = new HttpClient(handler)) 24 | { 25 | client.Timeout = timeOut; 26 | 27 | try 28 | { 29 | result.Response = await client.SendAsync(requestMessage, cancellationToken); 30 | } 31 | catch (TaskCanceledException) 32 | { 33 | result.ErrorMessage = $"Request timed out after {timeOut.TotalSeconds}"; 34 | } 35 | catch (Exception ex) 36 | { 37 | result.ErrorMessage = ex.InnerException != null ? ex.InnerException.Message : ex.Message; 38 | } 39 | } 40 | 41 | return result; 42 | } 43 | 44 | private static HttpRequestMessage BuildRequest(Request request, RequestResult result) 45 | { 46 | var url = request.Url?.ExpandVariables().Trim(); 47 | HttpMethod method = GetMethod(request.Method?.Text); 48 | 49 | var message = new HttpRequestMessage(method, url); ; 50 | 51 | try 52 | { 53 | AddBody(request, message); 54 | AddHeaders(request, message); 55 | } 56 | catch (Exception ex) 57 | { 58 | result.ErrorMessage = ex.Message; 59 | } 60 | 61 | return message; 62 | } 63 | 64 | private static void AddBody(Request request, HttpRequestMessage message) 65 | { 66 | if (request.Body == null) 67 | { 68 | return; 69 | } 70 | 71 | if (message.Method == HttpMethod.Get) 72 | { 73 | throw new HttpRequestException($"A request body is not supported for {message.Method} requests."); 74 | } 75 | 76 | message.Content = new StringContent(request.ExpandBodyVariables()); 77 | } 78 | 79 | public static void AddHeaders(Request request, HttpRequestMessage message) 80 | { 81 | if (request.Headers != null) 82 | { 83 | foreach (Header header in request.Headers) 84 | { 85 | var name = header?.Name?.ExpandVariables(); 86 | var value = header?.Value?.ExpandVariables(); 87 | 88 | if (name!.Equals("content-type", StringComparison.OrdinalIgnoreCase) && request.Body != null) 89 | { 90 | // Remove name-value pairs that can follow the MIME type 91 | string mimeType = value!.GetFirstToken(); 92 | 93 | message.Content = new StringContent(request.ExpandBodyVariables(), System.Text.Encoding.UTF8, mimeType); 94 | } 95 | 96 | message.Headers.TryAddWithoutValidation(name, value); 97 | } 98 | } 99 | 100 | if (!message.Headers.Contains("User-Agent")) 101 | { 102 | message.Headers.Add("User-Agent", nameof(RestClient)); 103 | } 104 | } 105 | 106 | private static HttpMethod GetMethod(string? methodName) 107 | { 108 | return methodName?.ToLowerInvariant() switch 109 | { 110 | "head" => HttpMethod.Head, 111 | "post" => HttpMethod.Post, 112 | "put" => HttpMethod.Put, 113 | "delete" => HttpMethod.Delete, 114 | "options" => HttpMethod.Options, 115 | "trace" => HttpMethod.Trace, 116 | "patch" => new HttpMethod("PATCH"), 117 | _ => HttpMethod.Get, 118 | }; 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/RestClient/Constants.cs: -------------------------------------------------------------------------------- 1 | namespace RestClient 2 | { 3 | public class Constants 4 | { 5 | public const char CommentChar = '#'; 6 | public const string MarketplaceId = "MadsKristensen.RestClient"; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/RestClient/Parser/Document.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | 4 | namespace RestClient 5 | { 6 | public partial class Document 7 | { 8 | private Dictionary? _variables = null; 9 | private string[] _lines; 10 | 11 | protected Document(string[] lines) 12 | { 13 | _lines = lines; 14 | Parse(); 15 | } 16 | 17 | public List Items { get; private set; } = new List(); 18 | 19 | public Dictionary? VariablesExpanded => _variables; 20 | 21 | public List Requests { get; private set; } = new(); 22 | public List Variables { get; private set; } = new(); 23 | 24 | public void UpdateLines(string[] lines) 25 | { 26 | _lines = lines; 27 | } 28 | 29 | public static Document FromLines(params string[] lines) 30 | { 31 | var doc = new Document(lines); 32 | return doc; 33 | } 34 | 35 | private void ExpandVariables() 36 | { 37 | Dictionary expandedVars = new(); 38 | 39 | foreach (Variable variable in Variables) 40 | { 41 | var value = variable.Value.Text; 42 | 43 | foreach (var key in expandedVars.Keys) 44 | { 45 | value = value.Replace("{{" + key + "}}", expandedVars[key].Trim()); 46 | } 47 | 48 | expandedVars[variable.Name.Text.Substring(1)] = value; 49 | } 50 | 51 | _variables = expandedVars; 52 | } 53 | 54 | public ParseItem? FindItemFromPosition(int position) 55 | { 56 | ParseItem? item = Items.LastOrDefault(t => t.Contains(position)); 57 | ParseItem? reference = item?.References.FirstOrDefault(v => v != null && v.Contains(position)); 58 | 59 | // Return the reference if it exist; otherwise the item 60 | return reference ?? item; 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/RestClient/Parser/DocumentParser.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text.RegularExpressions; 5 | 6 | namespace RestClient 7 | { 8 | public partial class Document 9 | { 10 | private static readonly Regex _regexUrl = new(@"^((?get|post|patch|put|delete|head|options|trace))\s*(?[^\s]+)\s*(?HTTP/.*)?", RegexOptions.IgnoreCase | RegexOptions.Compiled); 11 | private static readonly Regex _regexHeader = new(@"^(?[^\s]+)?([\s]+)?(?:)(?.+)", RegexOptions.Compiled); 12 | private static readonly Regex _regexVariable = new(@"^(?@[^\s]+)\s*(?=)\s*(?.+)", RegexOptions.Compiled); 13 | private static readonly Regex _regexRef = new(@"{{[\w]+}}", RegexOptions.Compiled); 14 | 15 | public bool IsParsing { get; private set; } 16 | public bool IsValid { get; private set; } 17 | 18 | public void Parse() 19 | { 20 | IsParsing = true; 21 | var isSuccess = false; 22 | var start = 0; 23 | 24 | try 25 | { 26 | List tokens = new(); 27 | 28 | foreach (var line in _lines) 29 | { 30 | IEnumerable? current = ParseLine(start, line, tokens); 31 | 32 | if (current != null) 33 | { 34 | tokens.AddRange(current); 35 | } 36 | 37 | start += line.Length; 38 | } 39 | 40 | Items = tokens; 41 | 42 | OrganizeItems(); 43 | ExpandVariables(); 44 | ValidateDocument(); 45 | 46 | isSuccess = true; 47 | } 48 | finally 49 | { 50 | IsParsing = false; 51 | 52 | if (isSuccess) 53 | { 54 | Parsed?.Invoke(this, EventArgs.Empty); 55 | } 56 | } 57 | } 58 | 59 | private IEnumerable ParseLine(int start, string line, List tokens) 60 | { 61 | var trimmedLine = line.Trim(); 62 | List items = new(); 63 | 64 | // Comment 65 | if (trimmedLine.StartsWith(Constants.CommentChar.ToString())) 66 | { 67 | items.Add(ToParseItem(line, start, ItemType.Comment, false)); 68 | } 69 | // Request body 70 | else if (IsBodyToken(line, tokens)) 71 | { 72 | items.Add(ToParseItem(line, start, ItemType.Body)); 73 | } 74 | // Empty line 75 | else if (string.IsNullOrWhiteSpace(line)) 76 | { 77 | items.Add(ToParseItem(line, start, ItemType.EmptyLine)); 78 | } 79 | // Variable declaration 80 | else if (IsMatch(_regexVariable, trimmedLine, out Match matchVar)) 81 | { 82 | items.Add(ToParseItem(matchVar, start, "name", ItemType.VariableName, false)!); 83 | items.Add(ToParseItem(matchVar, start, "value", ItemType.VariableValue, true)!); 84 | } 85 | // Request URL 86 | else if (IsMatch(_regexUrl, trimmedLine, out Match matchUrl)) 87 | { 88 | ParseItem method = ToParseItem(matchUrl, start, "method", ItemType.Method)!; 89 | ParseItem url = ToParseItem(matchUrl, start, "url", ItemType.Url)!; 90 | ParseItem? version = ToParseItem(matchUrl, start, "version", ItemType.Version); 91 | items.Add(new Request(this, method, url, version)); 92 | items.Add(method); 93 | items.Add(url); 94 | 95 | if (version != null) 96 | { 97 | items.Add(version); 98 | } 99 | } 100 | // Header 101 | else if (tokens.Count > 0 && IsMatch(_regexHeader, trimmedLine, out Match matchHeader)) 102 | { 103 | ParseItem? prev = tokens.Last(); 104 | if (prev?.Type == ItemType.HeaderValue || prev?.Type == ItemType.Url || prev?.Type == ItemType.Version || prev?.Type == ItemType.Comment) 105 | { 106 | items.Add(ToParseItem(matchHeader, start, "name", ItemType.HeaderName)!); 107 | items.Add(ToParseItem(matchHeader, start, "value", ItemType.HeaderValue)!); 108 | } 109 | } 110 | 111 | return items; 112 | } 113 | 114 | private bool IsBodyToken(string line, List tokens) 115 | { 116 | ParseItem? prev = tokens.LastOrDefault(); 117 | 118 | if (prev != null && string.IsNullOrWhiteSpace(prev.Text) && string.IsNullOrWhiteSpace(line)) 119 | { 120 | return false; 121 | } 122 | 123 | if (prev?.Type == ItemType.Body) 124 | { 125 | return true; 126 | } 127 | 128 | if (prev?.Type != ItemType.EmptyLine) 129 | { 130 | return false; 131 | } 132 | 133 | ParseItem? parent = tokens.ElementAtOrDefault(Math.Max(0, tokens.Count - 2)); 134 | 135 | if (parent?.Type == ItemType.HeaderValue || parent?.Type == ItemType.Url || parent?.Type == ItemType.Version || (parent?.Type == ItemType.Comment && parent?.TextExcludingLineBreaks != "###")) 136 | { 137 | return true; 138 | } 139 | 140 | return false; 141 | } 142 | 143 | public static bool IsMatch(Regex regex, string line, out Match match) 144 | { 145 | match = regex.Match(line); 146 | return match.Success; 147 | } 148 | 149 | private ParseItem ToParseItem(string line, int start, ItemType type, bool supportsVariableReferences = true) 150 | { 151 | var item = new ParseItem(start, line, this, type); 152 | 153 | if (supportsVariableReferences) 154 | { 155 | AddVariableReferences(item); 156 | } 157 | 158 | return item; 159 | } 160 | 161 | private ParseItem? ToParseItem(Match match, int start, string groupName, ItemType type, bool supportsVariableReferences = true) 162 | { 163 | Group? group = match.Groups[groupName]; 164 | 165 | if (string.IsNullOrEmpty(group.Value)) 166 | { 167 | return null; 168 | } 169 | 170 | return ToParseItem(group.Value, start + group.Index, type, supportsVariableReferences); 171 | } 172 | 173 | private void AddVariableReferences(ParseItem token) 174 | { 175 | foreach (Match match in _regexRef.Matches(token.Text)) 176 | { 177 | ParseItem? reference = ToParseItem(match.Value, token.Start + match.Index, ItemType.Reference, false); 178 | token.References.Add(reference); 179 | } 180 | } 181 | 182 | private void ValidateDocument() 183 | { 184 | IsValid = true; 185 | foreach (ParseItem item in Items) 186 | { 187 | // Variable references 188 | foreach (ParseItem? reference in item.References) 189 | { 190 | if (VariablesExpanded != null && !VariablesExpanded.ContainsKey(reference.Text.Trim('{', '}'))) 191 | { 192 | reference.Errors.Add(Errors.PL001.WithFormat(reference.Text.Trim('{', '}'))); 193 | IsValid = false; 194 | } 195 | } 196 | 197 | // URLs 198 | if (item.Type == ItemType.Url) 199 | { 200 | var uri = item.ExpandVariables(); 201 | 202 | if (!Uri.TryCreate(uri, UriKind.Absolute, out _)) 203 | { 204 | item.Errors.Add(Errors.PL002.WithFormat(uri)); 205 | IsValid = false; 206 | } 207 | } 208 | } 209 | } 210 | 211 | private class Errors 212 | { 213 | public static Error PL001 { get; } = new("PL001", "The variable \"{0}\" is not defined.", ErrorCategory.Warning); 214 | public static Error PL002 { get; } = new("PL002", "\"{0}\" is not a valid absolute URI", ErrorCategory.Warning); 215 | } 216 | 217 | private void OrganizeItems() 218 | { 219 | Request? currentRequest = null; 220 | List requests = new(); 221 | List variables = new(); 222 | 223 | bool isWwwForm = false; 224 | 225 | foreach (ParseItem? item in Items) 226 | { 227 | if (item.Type == ItemType.VariableName) 228 | { 229 | var variable = new Variable(item, item.Next!); 230 | variables.Add(variable); 231 | } 232 | 233 | else if (item.Type == ItemType.Method) 234 | { 235 | currentRequest = (Request)item.Previous!; 236 | 237 | requests.Add(currentRequest); 238 | currentRequest?.Children?.Add(currentRequest.Method); 239 | currentRequest?.Children?.Add(currentRequest.Url); 240 | 241 | if (currentRequest?.Version != null) 242 | { 243 | currentRequest?.Children?.Add(currentRequest.Version); 244 | } 245 | } 246 | 247 | else if (currentRequest != null) 248 | { 249 | if (item.Type == ItemType.HeaderName) 250 | { 251 | var header = new Header(item, item.Next!); 252 | 253 | currentRequest?.Headers?.Add(header); 254 | currentRequest?.Children?.Add(header.Name); 255 | currentRequest?.Children?.Add(header.Value); 256 | 257 | isWwwForm |= IsWwwFormContentHeader(header); 258 | } 259 | else if (item.Type == ItemType.Body) 260 | { 261 | if (string.IsNullOrWhiteSpace(item.Text)) 262 | { 263 | continue; 264 | } 265 | 266 | var prevEmptyLine = item.Previous?.Type == ItemType.Body && string.IsNullOrWhiteSpace(item.Previous.Text) ? item.Previous.Text : ""; 267 | string content = isWwwForm ? item.TextExcludingLineBreaks : item.Text; 268 | currentRequest.Body += prevEmptyLine + content; 269 | currentRequest?.Children?.Add(item); 270 | } 271 | else if (item?.Type == ItemType.Comment) 272 | { 273 | if (item.Text.StartsWith("###")) 274 | { 275 | currentRequest = null; 276 | } 277 | } 278 | } 279 | } 280 | 281 | Variables = variables; 282 | Requests = requests; 283 | } 284 | 285 | private bool IsWwwFormContentHeader(Header header) 286 | { 287 | return header.Name.Text.IsTokenMatch("content-type") && 288 | header.Value.Text.GetFirstToken().IsTokenMatch("application/x-www-form-urlencoded"); 289 | } 290 | 291 | public event EventHandler? Parsed; 292 | } 293 | } 294 | -------------------------------------------------------------------------------- /src/RestClient/Parser/Header.cs: -------------------------------------------------------------------------------- 1 | namespace RestClient 2 | { 3 | public class Header 4 | { 5 | public Header(ParseItem name, ParseItem value) 6 | { 7 | Name = name; 8 | Value = value; 9 | } 10 | 11 | public ParseItem Name { get; } 12 | public ParseItem Value { get; } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/RestClient/Parser/ItemType.cs: -------------------------------------------------------------------------------- 1 | namespace RestClient 2 | { 3 | public enum ItemType 4 | { 5 | VariableName, 6 | VariableValue, 7 | Comment, 8 | Method, 9 | Url, 10 | Version, 11 | HeaderName, 12 | HeaderValue, 13 | Body, 14 | Reference, 15 | EmptyLine, 16 | Request, 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/RestClient/Parser/ParseItem.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | 4 | namespace RestClient 5 | { 6 | public class ParseItem 7 | { 8 | public HashSet _errors = new(); 9 | 10 | public ParseItem(int start, string text, Document document, ItemType type) 11 | { 12 | Start = start; 13 | Text = text; 14 | TextExcludingLineBreaks = text.TrimEnd(); 15 | Document = document; 16 | Type = type; 17 | } 18 | 19 | public ItemType Type { get; } 20 | 21 | public virtual int Start { get; } 22 | 23 | public virtual string Text { get; protected set; } 24 | public virtual string TextExcludingLineBreaks { get; protected set; } 25 | 26 | public Document Document { get; } 27 | 28 | public virtual int End => Start + Text.Length; 29 | public virtual int EndExcludingLineBreaks => Start + TextExcludingLineBreaks.Length; 30 | 31 | public virtual int Length => End - Start; 32 | public virtual int LengthExcludingLineBreaks => EndExcludingLineBreaks - Start; 33 | 34 | public List References { get; } = new(); 35 | 36 | public ICollection Errors => _errors; 37 | 38 | public bool IsValid => _errors.Count == 0; 39 | 40 | public virtual bool Contains(int position) 41 | { 42 | return Start <= position && End >= position; 43 | } 44 | 45 | public ParseItem? Previous 46 | { 47 | get 48 | { 49 | var index = Document.Items.IndexOf(this); 50 | return index > 0 ? Document.Items[index - 1] : null; 51 | } 52 | } 53 | 54 | public ParseItem? Next 55 | { 56 | get 57 | { 58 | var index = Document.Items.IndexOf(this); 59 | return Document.Items.ElementAtOrDefault(index + 1); 60 | } 61 | } 62 | 63 | public string ExpandVariables() 64 | { 65 | var clean = Text; 66 | 67 | if (Document.VariablesExpanded != null) 68 | { 69 | foreach (var key in Document.VariablesExpanded.Keys) 70 | { 71 | clean = clean.Replace("{{" + key + "}}", Document.VariablesExpanded[key].Trim()); 72 | } 73 | } 74 | 75 | return clean.Trim(); 76 | } 77 | 78 | public override string ToString() 79 | { 80 | return Type + " " + Text; 81 | } 82 | } 83 | 84 | public class Error 85 | { 86 | public Error(string errorCode, string message, ErrorCategory severity) 87 | { 88 | ErrorCode = errorCode; 89 | Message = message; 90 | Severity = severity; 91 | } 92 | 93 | public string ErrorCode { get; } 94 | public string Message { get; } 95 | public ErrorCategory Severity { get; } 96 | 97 | public Error WithFormat(params string[] replacements) 98 | { 99 | return new Error(ErrorCode, string.Format(Message, replacements), Severity); 100 | } 101 | } 102 | 103 | public enum ErrorCategory 104 | { 105 | Error, 106 | Warning, 107 | Message, 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/RestClient/Parser/Request.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | 6 | namespace RestClient 7 | { 8 | public class Request : ParseItem 9 | { 10 | private readonly Document _document; 11 | 12 | public Request(Document document, ParseItem method, ParseItem url, ParseItem? version) : base(method.Start, method.Text, document, ItemType.Request) 13 | { 14 | _document = document; 15 | Method = method; 16 | Url = url; 17 | Version = version; 18 | } 19 | 20 | public List? Children { get; set; } = new List(); 21 | 22 | public ParseItem Method { get; } 23 | public ParseItem Url { get; } 24 | 25 | public ParseItem? Version { get; } 26 | 27 | public List
? Headers { get; } = new(); 28 | 29 | public string? Body { get; set; } 30 | public override int Start => Method?.Start ?? 0; 31 | public override int End => Children.LastOrDefault()?.End ?? 0; 32 | public bool IsActive { get; set; } 33 | public bool LastRunWasSuccess { get; set; } = true; 34 | private Action _completionAction { get; set; } 35 | 36 | public void StartActive(Action OnCompletion) 37 | { 38 | _completionAction = OnCompletion; 39 | IsActive = true; 40 | LastRunWasSuccess = true; 41 | } 42 | public void EndActive(bool IsSuccessStatus) 43 | { 44 | IsActive = false; 45 | LastRunWasSuccess = IsSuccessStatus; 46 | _completionAction?.Invoke(); 47 | } 48 | 49 | public override string ToString() 50 | { 51 | StringBuilder sb = new(); 52 | 53 | sb.AppendLine($"{Method?.Text} {Url?.ExpandVariables()}"); 54 | 55 | foreach (Header header in Headers!) 56 | { 57 | sb.AppendLine($"{header?.Name?.ExpandVariables()}: { header?.Value?.ExpandVariables()}"); 58 | } 59 | 60 | if (!string.IsNullOrEmpty(Body)) 61 | { 62 | sb.AppendLine(ExpandBodyVariables()); 63 | } 64 | 65 | return sb.ToString().Trim(); 66 | } 67 | 68 | public string ExpandBodyVariables() 69 | { 70 | if (Body == null) 71 | { 72 | return ""; 73 | } 74 | 75 | // Then replace the references with the expanded values 76 | var clean = Body; 77 | 78 | if (_document.VariablesExpanded != null) 79 | { 80 | foreach (var key in _document.VariablesExpanded.Keys) 81 | { 82 | clean = clean.Replace("{{" + key + "}}", _document.VariablesExpanded[key].Trim()); 83 | } 84 | } 85 | 86 | return clean.Trim(); 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/RestClient/Parser/StringExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace RestClient 4 | { 5 | public static class StringExtensions 6 | { 7 | /// 8 | /// Performs culture-invariant, case insensitive comparison to see if a string 9 | /// is a match for the supplied token string. The test string is trimmed of 10 | /// spaces first. 11 | /// 12 | public static bool IsTokenMatch(this string input, string token) => 13 | string.Compare(input.Trim(), token, StringComparison.InvariantCultureIgnoreCase) == 0; 14 | 15 | /// 16 | /// Returns the first token from a list of tokens with the specified separator. 17 | /// This is used mainly to fetch the MIME type from content-type headers. 18 | /// 19 | public static string GetFirstToken(this string input, char separator = ';') => 20 | input.Split(separator)[0]; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/RestClient/Parser/Variable.cs: -------------------------------------------------------------------------------- 1 | namespace RestClient 2 | { 3 | public class Variable 4 | { 5 | public Variable(ParseItem name, ParseItem value) 6 | { 7 | Name = name; 8 | Value = value; 9 | } 10 | 11 | public ParseItem Name { get; } 12 | public ParseItem Value { get; } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/RestClient/RestClient.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | 10 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/RestClientVS/Commands/CommentCommand.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.ObjectModel; 2 | using System.ComponentModel.Composition; 3 | using System.Linq; 4 | using Microsoft.VisualStudio.Commanding; 5 | using Microsoft.VisualStudio.Text; 6 | using Microsoft.VisualStudio.Text.Editor; 7 | using Microsoft.VisualStudio.Text.Editor.Commanding.Commands; 8 | using Microsoft.VisualStudio.Text.Formatting; 9 | using Microsoft.VisualStudio.Utilities; 10 | 11 | namespace RestClientVS.Commands 12 | { 13 | [Export(typeof(ICommandHandler))] 14 | [Name(nameof(CommentCommand))] 15 | [ContentType(LanguageFactory.LanguageName)] 16 | [TextViewRole(PredefinedTextViewRoles.PrimaryDocument)] 17 | public class CommentCommand : ICommandHandler 18 | { 19 | public string DisplayName => nameof(CommentCommand); 20 | 21 | public bool ExecuteCommand(CommentSelectionCommandArgs args, CommandExecutionContext executionContext) 22 | { 23 | SnapshotSpan spans = args.TextView.Selection.SelectedSpans.First(); 24 | Collection lines = args.TextView.TextViewLines.GetTextViewLinesIntersectingSpan(spans); 25 | 26 | foreach (ITextViewLine line in lines.Reverse()) 27 | { 28 | args.TextView.TextBuffer.Insert(line.Start.Position, RestClient.Constants.CommentChar.ToString()); 29 | } 30 | 31 | return true; 32 | } 33 | 34 | public CommandState GetCommandState(CommentSelectionCommandArgs args) 35 | { 36 | return CommandState.Available; 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /src/RestClientVS/Commands/Commenting.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.ObjectModel; 2 | using System.Linq; 3 | using Microsoft.VisualStudio; 4 | using Microsoft.VisualStudio.Text; 5 | using Microsoft.VisualStudio.Text.Formatting; 6 | using RestClient; 7 | 8 | namespace RestClientVS 9 | { 10 | public class Commenting 11 | { 12 | public static async Task InitializeAsync() 13 | { 14 | // We need to manually intercept the commenting command, because language services swallow these commands. 15 | await VS.Commands.InterceptAsync(VSConstants.VSStd2KCmdID.COMMENT_BLOCK, () => Execute(Comment)); 16 | await VS.Commands.InterceptAsync(VSConstants.VSStd2KCmdID.UNCOMMENT_BLOCK, () => Execute(Uncomment)); 17 | } 18 | 19 | private static CommandProgression Execute(Action action) 20 | { 21 | return ThreadHelper.JoinableTaskFactory.Run(async () => 22 | { 23 | DocumentView doc = await VS.Documents.GetActiveDocumentViewAsync(); 24 | 25 | if (doc?.TextBuffer != null && doc.TextBuffer.ContentType.IsOfType(LanguageFactory.LanguageName)) 26 | { 27 | action(doc); 28 | return CommandProgression.Stop; 29 | } 30 | 31 | return CommandProgression.Continue; 32 | }); 33 | } 34 | 35 | private static void Comment(DocumentView doc) 36 | { 37 | SnapshotSpan spans = doc.TextView.Selection.SelectedSpans.First(); 38 | Collection lines = doc.TextView.TextViewLines.GetTextViewLinesIntersectingSpan(spans); 39 | 40 | foreach (ITextViewLine line in lines.Reverse()) 41 | { 42 | doc.TextBuffer.Insert(line.Start.Position, Constants.CommentChar.ToString()); 43 | } 44 | } 45 | 46 | private static void Uncomment(DocumentView doc) 47 | { 48 | SnapshotSpan spans = doc.TextView.Selection.SelectedSpans.First(); 49 | Collection lines = doc.TextView.TextViewLines.GetTextViewLinesIntersectingSpan(spans); 50 | 51 | foreach (ITextViewLine line in lines.Reverse()) 52 | { 53 | var span = Span.FromBounds(line.Start, line.End); 54 | var originalText = doc.TextBuffer.CurrentSnapshot.GetText(span).TrimStart(Constants.CommentChar); 55 | Span commentCharSpan = new(span.Start, span.Length - originalText.Length); 56 | 57 | doc.TextBuffer.Delete(commentCharSpan); 58 | } 59 | } 60 | } 61 | } -------------------------------------------------------------------------------- /src/RestClientVS/Commands/GoToDefinitionCommand.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.Composition; 2 | using System.Linq; 3 | using Microsoft.VisualStudio.Commanding; 4 | using Microsoft.VisualStudio.Text; 5 | using Microsoft.VisualStudio.Text.Editor; 6 | using Microsoft.VisualStudio.Text.Editor.Commanding.Commands; 7 | using Microsoft.VisualStudio.Utilities; 8 | using RestClient; 9 | 10 | namespace RestClientVS.Commands 11 | { 12 | [Export(typeof(ICommandHandler))] 13 | [Name(nameof(CommentCommand))] 14 | [ContentType(LanguageFactory.LanguageName)] 15 | [TextViewRole(PredefinedTextViewRoles.PrimaryDocument)] 16 | public class GoToDefinitionCommand : ICommandHandler 17 | { 18 | public string DisplayName => nameof(GoToDefinitionCommand); 19 | 20 | public bool ExecuteCommand(GoToDefinitionCommandArgs args, CommandExecutionContext executionContext) 21 | { 22 | var position = args.TextView.Caret.Position.BufferPosition.Position; 23 | 24 | Document document = args.TextView.TextBuffer.GetRestDocument(); 25 | ParseItem token = document.FindItemFromPosition(position); 26 | 27 | if (token?.Type == ItemType.Reference) 28 | { 29 | var varName = token.Text.Trim('{', '}'); 30 | Variable definition = document.Variables.FirstOrDefault(v => v.Name.Text.Substring(1).Equals(varName, StringComparison.OrdinalIgnoreCase)); 31 | 32 | if (definition != null) 33 | { 34 | args.TextView.Caret.MoveTo(new SnapshotPoint(args.TextView.TextBuffer.CurrentSnapshot, definition.Name.Start)); 35 | } 36 | 37 | return true; 38 | } 39 | 40 | return false; 41 | } 42 | 43 | public CommandState GetCommandState(GoToDefinitionCommandArgs args) 44 | { 45 | return CommandState.Available; 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /src/RestClientVS/Commands/SendRequestCommand.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Threading; 3 | using RestClient; 4 | using RestClient.Client; 5 | 6 | namespace RestClientVS 7 | { 8 | [Command(PackageIds.SendRequest)] 9 | internal sealed class SendRequestCommand : BaseCommand 10 | { 11 | private static string _lastRequest; 12 | private static CancellationTokenSource _source; 13 | 14 | protected override async Task ExecuteAsync(OleMenuCmdEventArgs e) 15 | { 16 | DocumentView docView = await VS.Documents.GetActiveDocumentViewAsync(); 17 | 18 | if (docView != null) 19 | { 20 | var position = docView.TextView.Caret.Position.BufferPosition.Position; 21 | RestClient.Document doc = docView.TextBuffer.GetRestDocument(); 22 | Request request = doc.Requests.FirstOrDefault(r => r.Contains(position)); 23 | 24 | if (request != null) 25 | { 26 | _lastRequest = request.ToString(); 27 | _source?.Cancel(); 28 | _source = new CancellationTokenSource(); 29 | 30 | await VS.StatusBar.ShowMessageAsync($"Sending request to {request.Url.ExpandVariables()}..."); 31 | await VS.StatusBar.StartAnimationAsync(StatusAnimation.Sync); 32 | 33 | General options = await General.GetLiveInstanceAsync(); 34 | RequestResult result = await RequestSender.SendAsync(request, TimeSpan.FromSeconds(options.Timeout), _source.Token); 35 | 36 | if (!string.IsNullOrEmpty(_lastRequest) && result.RequestToken.ToString() != _lastRequest) 37 | { 38 | // Prohibits multiple requests from writing at the same time. 39 | return; 40 | } 41 | 42 | if (docView.TextView.Properties.TryGetProperty(typeof(ResponseMargin), out ResponseMargin margin)) 43 | { 44 | await margin.UpdateReponseAsync(result); 45 | } 46 | 47 | request.EndActive(result.Response?.IsSuccessStatusCode ?? false); 48 | await VS.StatusBar.ShowMessageAsync("Request completed"); 49 | await VS.StatusBar.EndAnimationAsync(StatusAnimation.Sync); 50 | } 51 | } 52 | } 53 | 54 | protected override Task InitializeCompletedAsync() 55 | { 56 | Command.Supported = false; 57 | return base.InitializeCompletedAsync(); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/RestClientVS/Commands/UncommentCommand.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.ObjectModel; 2 | using System.ComponentModel.Composition; 3 | using System.Linq; 4 | using Microsoft.VisualStudio.Commanding; 5 | using Microsoft.VisualStudio.Text; 6 | using Microsoft.VisualStudio.Text.Editor; 7 | using Microsoft.VisualStudio.Text.Editor.Commanding.Commands; 8 | using Microsoft.VisualStudio.Text.Formatting; 9 | using Microsoft.VisualStudio.Utilities; 10 | 11 | namespace RestClientVS.Commands 12 | { 13 | [Export(typeof(ICommandHandler))] 14 | [Name(nameof(CommentCommand))] 15 | [ContentType(LanguageFactory.LanguageName)] 16 | [TextViewRole(PredefinedTextViewRoles.PrimaryDocument)] 17 | public class UncommentCommand : ICommandHandler 18 | { 19 | public string DisplayName => nameof(UncommentCommand); 20 | 21 | public bool ExecuteCommand(UncommentSelectionCommandArgs args, CommandExecutionContext executionContext) 22 | { 23 | SnapshotSpan spans = args.TextView.Selection.SelectedSpans.First(); 24 | Collection lines = args.TextView.TextViewLines.GetTextViewLinesIntersectingSpan(spans); 25 | 26 | foreach (ITextViewLine line in lines.Reverse()) 27 | { 28 | var span = Span.FromBounds(line.Start, line.End); 29 | var text = args.TextView.TextBuffer.CurrentSnapshot.GetText(span).TrimStart(RestClient.Constants.CommentChar); 30 | args.TextView.TextBuffer.Replace(span, text); 31 | } 32 | 33 | return true; 34 | } 35 | 36 | public CommandState GetCommandState(UncommentSelectionCommandArgs args) 37 | { 38 | return CommandState.Available; 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /src/RestClientVS/Editor/DropdownBars.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Linq; 3 | using Microsoft.VisualStudio.Package; 4 | using Microsoft.VisualStudio.Text.Editor; 5 | using Microsoft.VisualStudio.TextManager.Interop; 6 | using RestClient; 7 | 8 | namespace RestClientVS 9 | { 10 | internal class DropdownBars : TypeAndMemberDropdownBars, IDisposable 11 | { 12 | private readonly LanguageService _languageService; 13 | private readonly IWpfTextView _textView; 14 | private readonly Document _document; 15 | private bool _disposed; 16 | private bool _bufferHasChanged; 17 | 18 | public DropdownBars(IVsTextView textView, LanguageService languageService) 19 | : base(languageService) 20 | { 21 | _languageService = languageService; 22 | _textView = textView.ToIWpfTextView(); 23 | _document = _textView.TextBuffer.GetRestDocument(); 24 | _document.Parsed += OnDocumentParsed; 25 | 26 | InitializeAsync(textView).FireAndForget(); 27 | } 28 | 29 | // This moves the caret to trigger initial drop down load 30 | private Task InitializeAsync(IVsTextView textView) 31 | { 32 | return ThreadHelper.JoinableTaskFactory.StartOnIdle(() => 33 | { 34 | textView.SendExplicitFocus(); 35 | _textView.Caret.MoveToNextCaretPosition(); 36 | _textView.Caret.PositionChanged += CaretPositionChanged; 37 | _textView.Caret.MoveToPreviousCaretPosition(); 38 | }).Task; 39 | } 40 | 41 | private void OnDocumentParsed(object sender, EventArgs e) 42 | { 43 | _bufferHasChanged = true; 44 | SynchronizeDropdowns(); 45 | } 46 | 47 | private void CaretPositionChanged(object sender, CaretPositionChangedEventArgs e) => SynchronizeDropdowns(); 48 | 49 | private void SynchronizeDropdowns() 50 | { 51 | if (!_document.IsParsing) 52 | { 53 | _languageService.SynchronizeDropdowns(); 54 | } 55 | } 56 | 57 | public override bool OnSynchronizeDropdowns(LanguageService languageService, IVsTextView textView, int line, int col, ArrayList dropDownTypes, ArrayList dropDownMembers, ref int selectedType, ref int selectedMember) 58 | { 59 | if (_bufferHasChanged || dropDownMembers.Count == 0) 60 | { 61 | dropDownMembers.Clear(); 62 | 63 | _document.Items.OfType() 64 | .Select(entry => CreateDropDownMember(entry, textView)) 65 | .ToList() 66 | .ForEach(ddm => dropDownMembers.Add(ddm)); 67 | } 68 | 69 | if (dropDownTypes.Count == 0) 70 | { 71 | var thisExt = $"{Vsix.Name} ({Vsix.Version})"; 72 | dropDownTypes.Add(new DropDownMember(thisExt, new TextSpan(), 126, DROPDOWNFONTATTR.FONTATTR_GRAY)); 73 | } 74 | 75 | DropDownMember currentDropDown = dropDownMembers 76 | .OfType() 77 | .Where(d => d.Span.iStartLine <= line) 78 | .LastOrDefault(); 79 | 80 | selectedMember = dropDownMembers.IndexOf(currentDropDown); 81 | selectedType = 0; 82 | _bufferHasChanged = false; 83 | 84 | return true; 85 | } 86 | 87 | private static DropDownMember CreateDropDownMember(Request request, IVsTextView textView) 88 | { 89 | TextSpan textSpan = GetTextSpan(request, textView); 90 | var text = request.Method.Text + " " + request.Url.Text + " " + request.Version?.Text; 91 | return new DropDownMember(text, textSpan, 126, DROPDOWNFONTATTR.FONTATTR_PLAIN); 92 | } 93 | 94 | private static TextSpan GetTextSpan(Request item, IVsTextView textView) 95 | { 96 | TextSpan textSpan = new(); 97 | 98 | textView.GetLineAndColumn(item.Method.Start, out textSpan.iStartLine, out textSpan.iStartIndex); 99 | textView.GetLineAndColumn(item.Url.End + 1, out textSpan.iEndLine, out textSpan.iEndIndex); 100 | 101 | return textSpan; 102 | } 103 | 104 | public void Dispose() 105 | { 106 | if (_disposed) 107 | { 108 | return; 109 | } 110 | 111 | _disposed = true; 112 | _textView.Caret.PositionChanged -= CaretPositionChanged; 113 | _document.Parsed -= OnDocumentParsed; 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/RestClientVS/Editor/EditorFeatures.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.ComponentModel.Composition; 3 | using Microsoft.VisualStudio.Imaging; 4 | using System.Linq; 5 | using System.Text.RegularExpressions; 6 | using System.Threading.Tasks; 7 | using Microsoft.VisualStudio.Language.Intellisense; 8 | using Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion; 9 | using Microsoft.VisualStudio.Language.StandardClassification; 10 | using Microsoft.VisualStudio.Text; 11 | using Microsoft.VisualStudio.Text.BraceCompletion; 12 | using Microsoft.VisualStudio.Text.Editor; 13 | using Microsoft.VisualStudio.Text.Tagging; 14 | using Microsoft.VisualStudio.Utilities; 15 | using RestClient; 16 | 17 | namespace RestClientVS 18 | { 19 | [Export(typeof(ITaggerProvider))] 20 | [TagType(typeof(IClassificationTag))] 21 | [ContentType(LanguageFactory.LanguageName)] 22 | public class SyntaxHighligting : TokenClassificationTaggerBase 23 | { 24 | public override Dictionary ClassificationMap { get; } = new() 25 | { 26 | { ItemType.VariableName, PredefinedClassificationTypeNames.SymbolDefinition }, 27 | { ItemType.VariableValue, PredefinedClassificationTypeNames.Text }, 28 | { ItemType.Method, PredefinedClassificationTypeNames.MarkupNode }, 29 | { ItemType.Url, PredefinedClassificationTypeNames.Text }, 30 | { ItemType.Version, PredefinedClassificationTypeNames.Type }, 31 | { ItemType.HeaderName, PredefinedClassificationTypeNames.Identifier }, 32 | { ItemType.HeaderValue, PredefinedClassificationTypeNames.Literal }, 33 | { ItemType.Comment, PredefinedClassificationTypeNames.Comment }, 34 | { ItemType.Body, PredefinedClassificationTypeNames.Text }, 35 | { ItemType.Reference, PredefinedClassificationTypeNames.MarkupAttribute }, 36 | }; 37 | } 38 | 39 | [Export(typeof(ITaggerProvider))] 40 | [TagType(typeof(IStructureTag))] 41 | [ContentType(LanguageFactory.LanguageName)] 42 | public class Outlining : TokenOutliningTaggerBase 43 | { } 44 | 45 | [Export(typeof(ITaggerProvider))] 46 | [TagType(typeof(IErrorTag))] 47 | [ContentType(LanguageFactory.LanguageName)] 48 | public class ErrorSquigglies : TokenErrorTaggerBase 49 | { } 50 | 51 | [Export(typeof(IAsyncQuickInfoSourceProvider))] 52 | [ContentType(LanguageFactory.LanguageName)] 53 | internal sealed class Tooltips : TokenQuickInfoBase 54 | { } 55 | 56 | [Export(typeof(IBraceCompletionContextProvider))] 57 | [BracePair('(', ')')] 58 | [BracePair('[', ']')] 59 | [BracePair('{', '}')] 60 | [BracePair('"', '"')] 61 | [BracePair('$', '$')] 62 | [ContentType(LanguageFactory.LanguageName)] 63 | [ProvideBraceCompletion(LanguageFactory.LanguageName)] 64 | internal sealed class BraceCompletion : BraceCompletionBase 65 | { } 66 | 67 | [Export(typeof(IAsyncCompletionCommitManagerProvider))] 68 | [ContentType(LanguageFactory.LanguageName)] 69 | internal sealed class CompletionCommitManager : CompletionCommitManagerBase 70 | { 71 | public override IEnumerable CommitChars => new char[] { ' ', '\'', '"', ',', '.', ';', ':', '\\', '$' }; 72 | } 73 | 74 | [Export(typeof(IViewTaggerProvider))] 75 | [TagType(typeof(TextMarkerTag))] 76 | [ContentType(LanguageFactory.LanguageName)] 77 | internal sealed class BraceMatchingTaggerProvider : BraceMatchingBase 78 | { 79 | // This will match parenthesis, curly brackets, and square brackets by default. 80 | // Override the BraceList property to modify the list of braces to match. 81 | } 82 | 83 | [Export(typeof(IViewTaggerProvider))] 84 | [ContentType(LanguageFactory.LanguageName)] 85 | [TagType(typeof(TextMarkerTag))] 86 | public class SameWordHighlighter : SameWordHighlighterBase 87 | { } 88 | 89 | [Export(typeof(IWpfTextViewCreationListener))] 90 | [ContentType(LanguageFactory.LanguageName)] 91 | [TextViewRole(PredefinedTextViewRoles.PrimaryDocument)] 92 | public class UserRating : WpfTextViewCreationListener 93 | { 94 | private readonly RatingPrompt _rating = new(Constants.MarketplaceId, Vsix.Name, General.Instance); 95 | private readonly DateTime _openedDate = DateTime.Now; 96 | 97 | protected override void Closed(IWpfTextView textView) 98 | { 99 | if (_openedDate.AddMinutes(2) < DateTime.Now) 100 | { 101 | // Only register use after the document was open for more than 2 minutes. 102 | _rating.RegisterSuccessfulUsage(); 103 | 104 | } 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/RestClientVS/Editor/IntelliSense.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Collections.Immutable; 3 | using System.ComponentModel.Composition; 4 | using System.Linq; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using Microsoft.VisualStudio.Core.Imaging; 8 | using Microsoft.VisualStudio.Imaging; 9 | using Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion; 10 | using Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Data; 11 | using Microsoft.VisualStudio.Text; 12 | using Microsoft.VisualStudio.Text.Adornments; 13 | using Microsoft.VisualStudio.Text.Editor; 14 | using Microsoft.VisualStudio.Text.Operations; 15 | using Microsoft.VisualStudio.Utilities; 16 | using RestClient; 17 | 18 | namespace RestClientVS.Completion 19 | { 20 | [Export(typeof(IAsyncCompletionSourceProvider))] 21 | [ContentType(LanguageFactory.LanguageName)] 22 | [Name(LanguageFactory.LanguageName)] 23 | internal class RestCompletionSourceProvider : IAsyncCompletionSourceProvider 24 | { 25 | [Import] internal ITextStructureNavigatorSelectorService _structureNavigator = null; 26 | 27 | public IAsyncCompletionSource GetOrCreate(ITextView textView) => 28 | textView.Properties.GetOrCreateSingletonProperty(() => new IntelliSense(_structureNavigator)); 29 | } 30 | 31 | public class IntelliSense : IAsyncCompletionSource 32 | { 33 | private readonly ITextStructureNavigatorSelectorService _structureNavigator; 34 | private static readonly ImageElement _httpMethodIcon = new(KnownMonikers.HTTPConnection.ToImageId(), "HTTP method"); 35 | private static readonly ImageElement _headerNameIcon = new(KnownMonikers.Metadata.ToImageId(), "HTTP header"); 36 | private static readonly ImageElement _referenceIcon = new(KnownMonikers.LocalVariable.ToImageId(), "Variable"); 37 | 38 | public IntelliSense(ITextStructureNavigatorSelectorService structureNavigator) 39 | { 40 | _structureNavigator = structureNavigator; 41 | } 42 | 43 | public Task GetCompletionContextAsync(IAsyncCompletionSession session, CompletionTrigger trigger, SnapshotPoint triggerLocation, SnapshotSpan applicableToSpan, CancellationToken cancellationToken) 44 | { 45 | ITextSnapshotLine line = triggerLocation.GetContainingLine(); 46 | 47 | RestClient.Document document = session.TextView.TextBuffer.GetRestDocument(); 48 | SnapshotPoint lineStart = line.Start; 49 | ParseItem token = GetPreviousToken(document, lineStart, out var hasEmptyLine); 50 | 51 | if (applicableToSpan.Start == lineStart) // only trigger on beginning of line 52 | { 53 | // HTTP Method 54 | if (token == null || token.Type == ItemType.VariableName || (token.Type == ItemType.Comment && token.Text.StartsWith("###"))) 55 | { 56 | return Task.FromResult(ConvertToCompletionItems(IntelliSenseCatalog.HttpMethods, _httpMethodIcon)); 57 | } 58 | 59 | // HTTP Headers 60 | if (!hasEmptyLine && (token.Type == ItemType.HeaderValue || token.Type == ItemType.Url)) 61 | { 62 | var spanBeforeCaret = new SnapshotSpan(lineStart, triggerLocation); 63 | var textBeforeCaret = triggerLocation.Snapshot.GetText(spanBeforeCaret); 64 | var colonIndex = textBeforeCaret.IndexOf(':'); 65 | var colonExistsBeforeCaret = colonIndex != -1; 66 | 67 | if (!colonExistsBeforeCaret) 68 | { 69 | return Task.FromResult(ConvertToCompletionItems(IntelliSenseCatalog.HttpHeaderNames, _headerNameIcon)); 70 | } 71 | } 72 | } 73 | 74 | // Variable references 75 | ParseItem currentToken = document.Items.LastOrDefault(v => v.Contains(triggerLocation.Position)); 76 | ParseItem currentReference = currentToken?.References.FirstOrDefault(v => v.Contains(triggerLocation.Position)); 77 | if (currentReference != null) 78 | { 79 | return Task.FromResult(GetReferenceCompletion(document.VariablesExpanded)); 80 | } 81 | 82 | //// User is likely in the key portion of the pair 83 | //if (!colonExistsBeforeCaret) 84 | //{ 85 | // return GetContextForKey(); 86 | //} 87 | 88 | //// User is likely in the value portion of the pair. Try to provide extra items based on the key. 89 | //var KeyExtractingRegex = new Regex(@"\W*(\w+s)\W*:"); 90 | //Match key = KeyExtractingRegex.Match(textBeforeCaret); 91 | //var candidateName = key.Success ? key.Groups.Count > 0 && key.Groups[1].Success ? key.Groups[1].Value : string.Empty : string.Empty; 92 | //return GetContextForValue(candidateName); 93 | 94 | return Task.FromResult(null); 95 | } 96 | 97 | private ParseItem GetPreviousToken(RestClient.Document document, SnapshotPoint point, out bool hasEmptyLine) 98 | { 99 | ParseItem current = null; 100 | hasEmptyLine = false; 101 | 102 | foreach (ParseItem token in document.Items) 103 | { 104 | if (token.End > point.Position) 105 | { 106 | break; 107 | } 108 | 109 | if (token?.Type != ItemType.EmptyLine) 110 | { 111 | current = token; 112 | } 113 | 114 | hasEmptyLine = token?.Type == ItemType.EmptyLine; 115 | } 116 | 117 | return current; 118 | } 119 | 120 | private CompletionContext ConvertToCompletionItems(IDictionary dic, ImageElement icon) 121 | { 122 | List items = new(); 123 | 124 | foreach (var key in dic.Keys) 125 | { 126 | var completion = new CompletionItem(key, this, icon); 127 | completion.Properties.AddProperty("description", dic[key]?.Trim()); 128 | items.Add(completion); 129 | } 130 | 131 | return new CompletionContext(items.ToImmutableArray()); 132 | } 133 | 134 | private CompletionContext GetReferenceCompletion(Dictionary variables) 135 | { 136 | List items = new(); 137 | foreach (var key in variables.Keys) 138 | { 139 | var completion = new CompletionItem(key, this, _referenceIcon, ImmutableArray.Empty, "", $"{{{{{key}}}}}", key, key, ImmutableArray.Empty); 140 | completion.Properties.AddProperty("description", variables[key]); 141 | items.Add(completion); 142 | } 143 | 144 | return new CompletionContext(items.ToImmutableArray()); 145 | } 146 | 147 | public Task GetDescriptionAsync(IAsyncCompletionSession session, CompletionItem item, CancellationToken token) 148 | { 149 | if (item.Properties.TryGetProperty("description", out string description)) 150 | { 151 | return Task.FromResult(description); 152 | } 153 | 154 | return Task.FromResult(null); 155 | } 156 | 157 | public CompletionStartData InitializeCompletion(CompletionTrigger trigger, SnapshotPoint triggerLocation, CancellationToken token) 158 | { 159 | // We don't trigger completion when user typed 160 | if (char.IsNumber(trigger.Character) // a number 161 | || char.IsPunctuation(trigger.Character) // punctuation 162 | || trigger.Character == '\n' // new line 163 | || trigger.Character == Constants.CommentChar 164 | || trigger.Reason == CompletionTriggerReason.Backspace 165 | || trigger.Reason == CompletionTriggerReason.Deletion) 166 | { 167 | return CompletionStartData.DoesNotParticipateInCompletion; 168 | } 169 | 170 | RestClient.Document document = triggerLocation.Snapshot.TextBuffer.GetRestDocument(); 171 | ParseItem item = document?.FindItemFromPosition(triggerLocation.Position); 172 | 173 | if (item?.Type == ItemType.Reference) 174 | { 175 | var tokenSpan = new SnapshotSpan(triggerLocation.Snapshot, item.ToSpan()); 176 | return new CompletionStartData(CompletionParticipation.ProvidesItems, tokenSpan); 177 | } 178 | else 179 | { 180 | SnapshotSpan tokenSpan = FindTokenSpanAtPosition(triggerLocation); 181 | 182 | if (triggerLocation.GetContainingLine().GetText().StartsWith(Constants.CommentChar.ToString(), StringComparison.Ordinal)) 183 | { 184 | return CompletionStartData.DoesNotParticipateInCompletion; 185 | } 186 | 187 | return new CompletionStartData(CompletionParticipation.ProvidesItems, tokenSpan); 188 | } 189 | } 190 | 191 | private SnapshotSpan FindTokenSpanAtPosition(SnapshotPoint triggerLocation) 192 | { 193 | // This method is not really related to completion, 194 | // we mostly work with the default implementation of ITextStructureNavigator 195 | // You will likely use the parser of your language 196 | ITextStructureNavigator navigator = _structureNavigator.GetTextStructureNavigator(triggerLocation.Snapshot.TextBuffer); 197 | TextExtent extent = navigator.GetExtentOfWord(triggerLocation); 198 | if (triggerLocation.Position > 0 && (!extent.IsSignificant || !extent.Span.GetText().Any(c => char.IsLetterOrDigit(c)))) 199 | { 200 | // Improves span detection over the default ITextStructureNavigation result 201 | extent = navigator.GetExtentOfWord(triggerLocation - 1); 202 | } 203 | 204 | ITrackingSpan tokenSpan = triggerLocation.Snapshot.CreateTrackingSpan(extent.Span, SpanTrackingMode.EdgeInclusive); 205 | 206 | ITextSnapshot snapshot = triggerLocation.Snapshot; 207 | var tokenText = tokenSpan.GetText(snapshot); 208 | if (string.IsNullOrWhiteSpace(tokenText)) 209 | { 210 | // The token at this location is empty. Return an empty span, which will grow as user types. 211 | return new SnapshotSpan(triggerLocation, 0); 212 | } 213 | 214 | // Trim quotes and new line characters. 215 | var startOffset = 0; 216 | var endOffset = 0; 217 | 218 | if (tokenText.Length > 0) 219 | { 220 | if (tokenText.StartsWith("\"")) 221 | { 222 | startOffset = 1; 223 | } 224 | } 225 | if (tokenText.Length - startOffset > 0) 226 | { 227 | if (tokenText.EndsWith("\"\r\n")) 228 | { 229 | endOffset = 3; 230 | } 231 | else if (tokenText.EndsWith("\r\n")) 232 | { 233 | endOffset = 2; 234 | } 235 | else if (tokenText.EndsWith("\"\n")) 236 | { 237 | endOffset = 2; 238 | } 239 | else if (tokenText.EndsWith("\n")) 240 | { 241 | endOffset = 1; 242 | } 243 | else if (tokenText.EndsWith("\"")) 244 | { 245 | endOffset = 1; 246 | } 247 | } 248 | 249 | return new SnapshotSpan(tokenSpan.GetStartPoint(snapshot) + startOffset, tokenSpan.GetEndPoint(snapshot) - endOffset); 250 | } 251 | } 252 | } 253 | -------------------------------------------------------------------------------- /src/RestClientVS/Editor/IntelliSenseCatalog.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace RestClientVS.Completion 4 | { 5 | public class IntelliSenseCatalog 6 | { 7 | public static readonly IDictionary HttpMethods = new Dictionary() 8 | { 9 | {"GET", "The GET method is used to retrieve information from the given server using a given URI.Requests using GET should only retrieve data and should have no other effect on the data."}, 10 | {"HEAD", "Same as GET, but transfers the status line and header section only."}, 11 | {"PATCH", "A PATCH request is considered a set of instructions on how to modify a resource. Contrast this with PUT; which is a complete representation of a resource."}, 12 | {"POST", "A POST request is used to send data to the server, for example, customer information, file upload, etc. using HTML forms"}, 13 | {"PUT", "Replaces all current representations of the target resource with the uploaded content."}, 14 | {"DELETE", "Removes all current representations of the target resource given by a URI."}, 15 | {"TRACE", "Performs a message loop-back test along the path to the target resource."}, 16 | {"OPTIONS", "Describes the communication options for the target resource."}, 17 | }; 18 | 19 | public static readonly IDictionary HttpHeaderNames = new Dictionary() 20 | { 21 | {"A-IM","Acceptable instance-manipulations for the request."}, 22 | {"Accept","Media type(s) that is/are acceptable for the response. See Content negotiation."}, 23 | {"Accept-Charset","Character sets that are acceptable."}, 24 | {"Accept-Datetime","Acceptable version in time."}, 25 | {"Accept-Encoding","List of acceptable encodings. See HTTP compression."}, 26 | {"Accept-Language","List of acceptable human languages for response. See Content negotiation."}, 27 | {"Access-Control-Request-Method,","Initiates a request for cross-origin resource sharing with Origin (below)."}, 28 | {"Access-Control-Request-Headers",""}, 29 | {"Authorization","Authentication credentials for HTTP authentication."}, 30 | {"Cache-Control","Used to specify directives that must be obeyed by all caching mechanisms along the request-response chain."}, 31 | {"Connection","Control options for the current connection and list of hop-by-hop request fields."}, 32 | {"Content-Encoding","The type of encoding used on the data. See HTTP compression."}, 33 | {"Content-Length","The length of the request body in octets (8-bit bytes)."}, 34 | {"Content-MD5","A Base64-encoded binary MD5 sum of the content of the request body."}, 35 | {"Content-Type","The Media type of the body of the request (used with POST and PUT requests)."}, 36 | {"Cookie","An HTTP cookie previously sent by the server with Set-Cookie (below)."}, 37 | {"Date","The date and time at which the message was originated (in HTTP-date format as defined by RFC 7231 Date/Time Formats)."}, 38 | {"Expect","Indicates that particular server behaviors are required by the client."}, 39 | {"Forwarded","Disclose original information of a client connecting to a web server through an HTTP proxy."}, 40 | {"From","The email address of the user making the request."}, 41 | {"Host","The domain name of the server (for virtual hosting), and the TCP port number on which the server is listening. The port number may be omitted if the port is the standard port for the service requested."}, 42 | {"HTTP2-Settings","A request that upgrades from HTTP/1.1 to HTTP/2 MUST include exactly one HTTP2-Setting header field. The HTTP2-Settings header field is a connection-specific header field that includes parameters that govern the HTTP/2 connection, provided in anticipation of the server accepting the request to upgrade."}, 43 | {"If-Match","Only perform the action if the client supplied entity matches the same entity on the server. This is mainly for methods like PUT to only update a resource if it has not been modified since the user last updated it."}, 44 | {"If-Modified-Since","Allows a 304 Not Modified to be returned if content is unchanged."}, 45 | {"If-None-Match","Allows a 304 Not Modified to be returned if content is unchanged, see HTTP ETag."}, 46 | {"If-Range","If the entity is unchanged, send me the part(s) that I am missing; otherwise, send me the entire new entity."}, 47 | {"If-Unmodified-Since","Only send the response if the entity has not been modified since a specific time."}, 48 | {"Max-Forwards","Limit the number of times the message can be forwarded through proxies or gateways."}, 49 | {"Origin","Initiates a request for cross-origin resource sharing (asks server for Access-Control-* response fields)."}, 50 | {"Pragma","Implementation-specific fields that may have various effects anywhere along the request-response chain."}, 51 | {"Prefer","Allows client to request that certain behaviors be employed by a server while processing a request."}, 52 | {"Proxy-Authorization","Authorization credentials for connecting to a proxy."}, 53 | {"Range","Request only part of an entity. Bytes are numbered from 0. See Byte serving."}, 54 | {"Referer","This is the address of the previous web page from which a link to the currently requested page was followed. (The word 'referrer' has been misspelled in the RFC as well as in most implementations to the point that it has become standard usage and is considered correct terminology)"}, 55 | {"TE","The transfer encodings the user agent is willing to accept: the same values as for the response header field Transfer-Encoding can be used, plus the 'trailers' value (related to the 'chunked' transfer method) to notify the server it expects to receive additional fields in the trailer after the last, zero-sized, chunk."}, 56 | {"Trailer","The Trailer general field value indicates that the given set of header fields is present in the trailer of a message encoded with chunked transfer coding."}, 57 | {"Transfer-Encoding","The form of encoding used to safely transfer the entity to the user. Currently defined methods are: chunked, compress, deflate, gzip, identity."}, 58 | {"User-Agent","The user agent string of the user agent."}, 59 | {"Upgrade","Ask the server to upgrade to another protocol."}, 60 | {"Via","Informs the server of proxies through which the request was sent."}, 61 | {"Warning","A general warning about possible problems with the entity body."}, 62 | }; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/RestClientVS/Editor/LanguageFactory.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.Design; 2 | using System.Runtime.InteropServices; 3 | using Microsoft.VisualStudio.Package; 4 | using Microsoft.VisualStudio.TextManager.Interop; 5 | 6 | namespace RestClientVS 7 | { 8 | [ComVisible(true)] 9 | [Guid(PackageGuids.RestEditorFactoryString)] 10 | public class LanguageFactory : LanguageBase 11 | { 12 | public const string LanguageName = "Rest"; 13 | public const string HttpFileExtension = ".http"; 14 | public const string RestFileExtension = ".rest"; 15 | 16 | private DropdownBars _dropdownBars; 17 | 18 | public LanguageFactory(object site) : base(site) 19 | { } 20 | 21 | public void RegisterLanguageService(Package package) 22 | { 23 | ((IServiceContainer)package).AddService(GetType(), this, true); 24 | } 25 | 26 | public override string Name => LanguageName; 27 | 28 | public override string[] FileExtensions => new[] { HttpFileExtension, RestFileExtension }; 29 | 30 | public override void SetDefaultPreferences(LanguagePreferences preferences) 31 | { 32 | preferences.EnableCodeSense = false; 33 | preferences.EnableMatchBraces = true; 34 | preferences.EnableMatchBracesAtCaret = true; 35 | preferences.EnableShowMatchingBrace = true; 36 | preferences.EnableCommenting = true; 37 | preferences.HighlightMatchingBraceFlags = _HighlightMatchingBraceFlags.HMB_USERECTANGLEBRACES; 38 | preferences.LineNumbers = true; 39 | preferences.MaxErrorMessages = 100; 40 | preferences.AutoOutlining = false; 41 | preferences.MaxRegionTime = 2000; 42 | preferences.InsertTabs = false; 43 | preferences.IndentSize = 2; 44 | preferences.IndentStyle = IndentingStyle.Smart; 45 | preferences.ShowNavigationBar = true; 46 | 47 | preferences.WordWrap = false; 48 | preferences.WordWrapGlyphs = true; 49 | 50 | preferences.AutoListMembers = true; 51 | preferences.EnableQuickInfo = true; 52 | preferences.ParameterInformation = true; 53 | } 54 | 55 | public override TypeAndMemberDropdownBars CreateDropDownHelper(IVsTextView textView) 56 | { 57 | _dropdownBars?.Dispose(); 58 | _dropdownBars = new DropdownBars(textView, this); 59 | 60 | return _dropdownBars; 61 | } 62 | 63 | public override void Dispose() 64 | { 65 | _dropdownBars?.Dispose(); 66 | _dropdownBars = null; 67 | base.Dispose(); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/RestClientVS/Editor/PlayButtonAdornment.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.ComponentModel.Composition; 3 | using System.Linq; 4 | using System.Windows; 5 | using System.Windows.Controls; 6 | using System.Windows.Input; 7 | using System.Windows.Media; 8 | using Microsoft.VisualStudio.Text; 9 | using Microsoft.VisualStudio.Text.Editor; 10 | using Microsoft.VisualStudio.Text.Tagging; 11 | using Microsoft.VisualStudio.Utilities; 12 | using RestClient; 13 | 14 | namespace RestClientVS.Language 15 | { 16 | [Export(typeof(IViewTaggerProvider))] 17 | [ContentType(LanguageFactory.LanguageName)] 18 | [TagType(typeof(IntraTextAdornmentTag))] 19 | internal sealed class RestIntratextAdornmentTaggerProvider : IViewTaggerProvider 20 | { 21 | public ITagger CreateTagger(ITextView textView, ITextBuffer buffer) where T : ITag => 22 | buffer.Properties.GetOrCreateSingletonProperty(() => new PlayButtonAdornment(buffer,textView)) as ITagger; 23 | } 24 | 25 | internal class PlayButtonAdornment : ITagger 26 | { 27 | private readonly ITextBuffer _buffer; 28 | private readonly ITextView _view; 29 | private readonly RestDocument _document; 30 | 31 | public PlayButtonAdornment(ITextBuffer buffer, ITextView view) 32 | { 33 | _buffer = buffer; 34 | _view = view; 35 | _document = buffer.GetRestDocument(); 36 | } 37 | 38 | public IEnumerable> GetTags(NormalizedSnapshotSpanCollection spans) 39 | { 40 | if (spans.Count == 0 || _document.IsParsing) 41 | { 42 | yield return null; 43 | } 44 | 45 | foreach (Request request in _document.Requests.Where(r => r.Url.IsValid)) 46 | { 47 | if (_buffer.CurrentSnapshot.Length >= request.Start) 48 | { 49 | IntraTextAdornmentTag tag = new(CreateUiControl(request), null, PositionAffinity.Successor); 50 | ITextSnapshotLine line = _buffer.CurrentSnapshot.GetLineFromPosition(request.Start); 51 | SnapshotSpan span = new(line.Snapshot, line.End, 0); 52 | 53 | yield return new TagSpan(span, tag); 54 | } 55 | } 56 | } 57 | 58 | private FrameworkElement CreateUiControl(Request request) 59 | { 60 | FrameworkElement element = new Label 61 | { 62 | Content = request.IsActive ? (char)0x231B : " ▶️", 63 | FontWeight = FontWeights.SemiBold, 64 | Foreground = request.LastRunWasSuccess ? Brushes.Green : Brushes.Red, 65 | Cursor = Cursors.Hand, 66 | Padding = new Thickness(0), 67 | Margin = new Thickness(4, -2, 0, 0), 68 | }; 69 | 70 | element.MouseLeftButtonUp += Element_MouseLeftButtonUp; 71 | 72 | return element; 73 | } 74 | 75 | private void Element_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) 76 | { 77 | var position = _view.Caret.Position.BufferPosition.Position; 78 | Document doc = _buffer.GetRestDocument(); 79 | Request request = doc.Requests.FirstOrDefault(r => r.Contains(position)); 80 | if (request != null) 81 | { 82 | request.StartActive(RaiseTagsChanged); 83 | RaiseTagsChanged(); 84 | VS.Commands.ExecuteAsync(PackageGuids.RestClientVS, PackageIds.SendRequest).FireAndForget(); 85 | } 86 | } 87 | 88 | private void RaiseTagsChanged() 89 | { 90 | var tempEvent = TagsChanged; 91 | if (tempEvent != null) 92 | tempEvent(this, new SnapshotSpanEventArgs(new SnapshotSpan(_buffer.CurrentSnapshot, 0, _buffer.CurrentSnapshot.Length))); 93 | } 94 | 95 | public event EventHandler TagsChanged; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/RestClientVS/Editor/TokenTagger.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.ComponentModel.Composition; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.VisualStudio.Core.Imaging; 6 | using Microsoft.VisualStudio.Imaging; 7 | using Microsoft.VisualStudio.Shell.Interop; 8 | using Microsoft.VisualStudio.Text; 9 | using Microsoft.VisualStudio.Text.Adornments; 10 | using Microsoft.VisualStudio.Text.Tagging; 11 | using Microsoft.VisualStudio.Utilities; 12 | using RestClient; 13 | 14 | namespace RestClientVS 15 | { 16 | [Export(typeof(ITaggerProvider))] 17 | [TagType(typeof(TokenTag))] 18 | [ContentType(LanguageFactory.LanguageName)] 19 | [Name(LanguageFactory.LanguageName)] 20 | internal sealed class TokenTaggerProvider : ITaggerProvider 21 | { 22 | public ITagger CreateTagger(ITextBuffer buffer) where T : ITag => 23 | buffer.Properties.GetOrCreateSingletonProperty(() => new TokenTagger(buffer)) as ITagger; 24 | } 25 | 26 | internal class TokenTagger : TokenTaggerBase, IDisposable 27 | { 28 | private readonly RestDocument _document; 29 | private static readonly ImageId _errorIcon = KnownMonikers.StatusWarningNoColor.ToImageId(); 30 | private bool _isDisposed; 31 | 32 | internal TokenTagger(ITextBuffer buffer) : base(buffer) 33 | { 34 | _document = buffer.GetRestDocument(); 35 | _document.Parsed += OnDocumentParsed; 36 | } 37 | 38 | private void OnDocumentParsed(object sender = null, EventArgs e = null) 39 | { 40 | _ = TokenizeAsync(); 41 | } 42 | 43 | public override Task TokenizeAsync() 44 | { 45 | // Make sure this is running on a background thread. 46 | ThreadHelper.ThrowIfOnUIThread(); 47 | 48 | List> list = new(); 49 | 50 | foreach (ParseItem item in _document.Items) 51 | { 52 | if (_document.IsParsing) 53 | { 54 | // Abort and wait for the next parse event to finish 55 | return Task.CompletedTask; 56 | } 57 | 58 | AddTagToList(list, item); 59 | 60 | foreach (ParseItem variable in item.References) 61 | { 62 | AddTagToList(list, variable); 63 | } 64 | } 65 | 66 | OnTagsUpdated(list); 67 | return Task.CompletedTask; 68 | } 69 | 70 | private void AddTagToList(List> list, ParseItem item) 71 | { 72 | var supportsOutlining = item is Request request && (request.Headers.Any() || request.Body != null); 73 | var hasTooltip = !item.IsValid; 74 | IEnumerable errors = CreateErrorListItem(item); 75 | TokenTag tag = CreateToken(item.Type, hasTooltip, supportsOutlining, errors); 76 | 77 | var span = new SnapshotSpan(Buffer.CurrentSnapshot, item.ToSpan()); 78 | list.Add(new TagSpan(span, tag)); 79 | } 80 | 81 | private IEnumerable CreateErrorListItem(ParseItem item) 82 | { 83 | if (!General.Instance.EnableValidation) 84 | { 85 | yield break; 86 | } 87 | 88 | ITextSnapshotLine line = Buffer.CurrentSnapshot.GetLineFromPosition(item.Start); 89 | 90 | foreach (Error error in item.Errors) 91 | { 92 | yield return new ErrorListItem 93 | { 94 | ProjectName = _document.ProjectName ?? "", 95 | FileName = _document.FileName, 96 | Message = error.Message, 97 | ErrorCategory = ConvertToVsCat(error.Severity), 98 | Severity = ConvertToVsSeverity(error.Severity), 99 | Line = line.LineNumber, 100 | Column = item.Start - line.Start.Position, 101 | BuildTool = Vsix.Name, 102 | ErrorCode = error.ErrorCode 103 | }; 104 | } 105 | } 106 | 107 | private static string ConvertToVsCat(ErrorCategory cat) 108 | { 109 | return cat switch 110 | { 111 | ErrorCategory.Message => PredefinedErrorTypeNames.Suggestion, 112 | ErrorCategory.Warning => PredefinedErrorTypeNames.Warning, 113 | _ => PredefinedErrorTypeNames.SyntaxError, 114 | }; 115 | } 116 | 117 | private static __VSERRORCATEGORY ConvertToVsSeverity(ErrorCategory cat) 118 | { 119 | return cat switch 120 | { 121 | ErrorCategory.Message => __VSERRORCATEGORY.EC_MESSAGE, 122 | ErrorCategory.Warning => __VSERRORCATEGORY.EC_WARNING, 123 | _ => __VSERRORCATEGORY.EC_ERROR, 124 | }; 125 | } 126 | 127 | public override Task GetTooltipAsync(SnapshotPoint triggerPoint) 128 | { 129 | ParseItem item = _document.FindItemFromPosition(triggerPoint.Position); 130 | 131 | // Error messages 132 | if (item?.IsValid == false) 133 | { 134 | var elm = new ContainerElement( 135 | ContainerElementStyle.Wrapped, 136 | new ImageElement(_errorIcon), 137 | string.Join(Environment.NewLine, item.Errors.Select(e => e.Message))); 138 | 139 | return Task.FromResult(elm); 140 | } 141 | 142 | return Task.FromResult(null); 143 | } 144 | 145 | public void Dispose() 146 | { 147 | if (!_isDisposed) 148 | { 149 | _document.Parsed -= OnDocumentParsed; 150 | _document.Dispose(); 151 | } 152 | 153 | _isDisposed = true; 154 | } 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /src/RestClientVS/ExtensionMethods.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.VisualStudio.Text; 2 | using RestClient; 3 | 4 | namespace RestClientVS 5 | { 6 | public static class ExtensionMethods 7 | { 8 | public static Span ToSpan(this ParseItem token) => 9 | new(token.Start, token.Length); 10 | 11 | public static RestDocument GetRestDocument(this ITextBuffer buffer) => 12 | buffer.Properties.GetOrCreateSingletonProperty(() => new RestDocument(buffer)); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/RestClientVS/Margin/ResponseControl.xaml: -------------------------------------------------------------------------------- 1 |  11 | 12 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/RestClientVS/Margin/ResponseControl.xaml.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | using System.Windows.Controls; 3 | using System.Xml.Linq; 4 | using Newtonsoft.Json.Linq; 5 | using RestClient.Client; 6 | 7 | namespace RestClientVS.Margin 8 | { 9 | public partial class ResponseControl : UserControl 10 | { 11 | public ResponseControl() 12 | { 13 | InitializeComponent(); 14 | } 15 | 16 | public async Task SetResponseTextAsync(RequestResult result) 17 | { 18 | if (result.Response != null) 19 | { 20 | var text = new StringBuilder(); 21 | text.AppendLine(result.Response.Headers.ToString()); 22 | text.AppendLine(result.Response.Content.Headers.ToString()); 23 | var mediaType = result.Response.Content.Headers?.ContentType?.MediaType; 24 | if (mediaType == null) 25 | { 26 | var rawString = await result.Response.Content.ReadAsStringAsync(); 27 | text.AppendLine("------------------------------------------------"); 28 | text.AppendLine($"StatusCode: {result.Response.StatusCode} ({(int)result.Response.StatusCode})"); 29 | if (!string.IsNullOrEmpty(rawString)) 30 | { 31 | text.AppendLine("Content:"); 32 | text.AppendLine(rawString); 33 | } 34 | 35 | Control.Text = text.ToString(); 36 | return; 37 | } 38 | if (mediaType.IndexOf("json", StringComparison.OrdinalIgnoreCase) > -1) 39 | { 40 | var jsonString = await result.Response.Content.ReadAsStringAsync(); 41 | try 42 | { 43 | var token = JToken.Parse(jsonString); 44 | text.AppendLine("------------------------------------------------"); 45 | text.AppendLine($"StatusCode: {result.Response.StatusCode} ({(int)result.Response.StatusCode})"); 46 | text.AppendLine("Content:"); 47 | text.AppendLine(token.ToString()); 48 | } 49 | catch (Exception ex) 50 | { 51 | text.AppendFormat("** {0} : Error parsing JSON ( {1} ), raw content follows. **\r\r", nameof(RestClientVS), ex.GetBaseException().Message); 52 | text.AppendLine(jsonString); 53 | } 54 | Control.Text = text.ToString(); 55 | return; 56 | } 57 | if (mediaType.IndexOf("xml", StringComparison.OrdinalIgnoreCase) > -1) 58 | { 59 | var xmlString = await result.Response.Content.ReadAsStringAsync(); 60 | try 61 | { 62 | var xmlElement = XElement.Parse(xmlString); 63 | text.AppendLine("------------------------------------------------"); 64 | text.AppendLine($"StatusCode: {result.Response.StatusCode} ({(int)result.Response.StatusCode})"); 65 | text.AppendLine("Content:"); 66 | text.AppendLine(xmlElement.ToString()); 67 | } 68 | catch (Exception ex) 69 | { 70 | text.AppendFormat("** {0} : Error parsing XML ( {1} ), raw content follows. **\r\r", nameof(RestClientVS), ex.GetBaseException().Message); 71 | text.AppendLine(xmlString); 72 | } 73 | Control.Text = text.ToString(); 74 | return; 75 | } 76 | Control.Text = await result.Response.ToRawStringAsync(); 77 | } 78 | else 79 | { 80 | Control.Text = result.ErrorMessage; 81 | } 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/RestClientVS/Margin/ResponseMargin.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | using System.Windows.Controls; 3 | using Microsoft.VisualStudio.Text.Editor; 4 | using RestClient.Client; 5 | using RestClientVS.Margin; 6 | 7 | namespace RestClientVS 8 | { 9 | public class ResponseMargin : DockPanel, IWpfTextViewMargin 10 | { 11 | private readonly ITextView _textView; 12 | private bool _isDisposed; 13 | private bool _isLoaded; 14 | 15 | public ResponseMargin(ITextView textview) 16 | { 17 | _textView = textview; 18 | } 19 | 20 | public ResponseControl Control = new(); 21 | 22 | public async Task UpdateReponseAsync(RequestResult result) 23 | { 24 | if (!_isLoaded) 25 | { 26 | await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); 27 | CreateRightMarginControls(); 28 | _isLoaded = true; 29 | } 30 | 31 | await Control.SetResponseTextAsync(result); 32 | } 33 | 34 | private void CreateRightMarginControls() 35 | { 36 | var width = General.Instance.ResponseWindowWidth; 37 | 38 | Grid grid = new(); 39 | grid.ColumnDefinitions.Add(new ColumnDefinition() { Width = new GridLength(0, GridUnitType.Star) }); 40 | grid.ColumnDefinitions.Add(new ColumnDefinition() { Width = new GridLength(5, GridUnitType.Pixel) }); 41 | grid.ColumnDefinitions.Add(new ColumnDefinition() { Width = new GridLength(width, GridUnitType.Pixel), MinWidth = 150 }); 42 | grid.RowDefinitions.Add(new RowDefinition()); 43 | Children.Add(grid); 44 | 45 | grid.Children.Add(Control); 46 | Grid.SetColumn(Control, 2); 47 | Grid.SetRow(Control, 0); 48 | 49 | GridSplitter splitter = new() 50 | { 51 | Width = 5, 52 | ResizeDirection = GridResizeDirection.Columns, 53 | VerticalAlignment = VerticalAlignment.Stretch, 54 | HorizontalAlignment = HorizontalAlignment.Stretch 55 | }; 56 | splitter.DragCompleted += RightDragCompleted; 57 | 58 | grid.Children.Add(splitter); 59 | Grid.SetColumn(splitter, 1); 60 | Grid.SetRow(splitter, 0); 61 | 62 | Action fixWidth = new(() => 63 | { 64 | // previewWindow maxWidth = current total width - textView minWidth 65 | var newWidth = (_textView.ViewportWidth + grid.ActualWidth) - 150; 66 | 67 | // preveiwWindow maxWidth < previewWindow minWidth 68 | if (newWidth < 150) 69 | { 70 | // Call 'get before 'set for performance 71 | if (grid.ColumnDefinitions[2].MinWidth != 0) 72 | { 73 | grid.ColumnDefinitions[2].MinWidth = 0; 74 | grid.ColumnDefinitions[2].MaxWidth = 0; 75 | } 76 | } 77 | else 78 | { 79 | grid.ColumnDefinitions[2].MaxWidth = newWidth; 80 | // Call 'get before 'set for performance 81 | if (grid.ColumnDefinitions[2].MinWidth == 0) 82 | { 83 | grid.ColumnDefinitions[2].MinWidth = 150; 84 | } 85 | } 86 | }); 87 | 88 | // Listen sizeChanged event of both marginGrid and textView 89 | grid.SizeChanged += (e, s) => fixWidth(); 90 | _textView.ViewportWidthChanged += (e, s) => fixWidth(); 91 | } 92 | 93 | private void RightDragCompleted(object sender, System.Windows.Controls.Primitives.DragCompletedEventArgs e) 94 | { 95 | if (!double.IsNaN(Control.ActualWidth)) 96 | { 97 | General.Instance.ResponseWindowWidth = (int)Control.ActualWidth; 98 | General.Instance.Save(); 99 | } 100 | } 101 | 102 | public FrameworkElement VisualElement => this; 103 | public double MarginSize => 0; 104 | public bool Enabled => true; 105 | 106 | public void Dispose() 107 | { 108 | if (!_isDisposed) 109 | { 110 | } 111 | 112 | _isDisposed = true; 113 | } 114 | 115 | public ITextViewMargin GetTextViewMargin(string marginName) => this; 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/RestClientVS/Margin/ResponseMarginProvider.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.Composition; 2 | using Microsoft.VisualStudio.Text.Editor; 3 | using Microsoft.VisualStudio.Utilities; 4 | 5 | namespace RestClientVS 6 | { 7 | [Export(typeof(IWpfTextViewMarginProvider))] 8 | [Name(nameof(ResponseMarginProvider))] 9 | [Order(After = PredefinedMarginNames.RightControl)] 10 | [MarginContainer(PredefinedMarginNames.Right)] 11 | [ContentType(LanguageFactory.LanguageName)] 12 | [TextViewRole(PredefinedTextViewRoles.Debuggable)] // This is to prevent the margin from loading in the diff view 13 | public class ResponseMarginProvider : IWpfTextViewMarginProvider 14 | { 15 | public IWpfTextViewMargin CreateMargin(IWpfTextViewHost wpfTextViewHost, IWpfTextViewMargin marginContainer) => 16 | wpfTextViewHost.TextView.Properties.GetOrCreateSingletonProperty(() => new ResponseMargin(wpfTextViewHost.TextView)); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/RestClientVS/Options/General.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | using System.Runtime.InteropServices; 3 | 4 | namespace RestClientVS 5 | { 6 | internal partial class OptionsProvider 7 | { 8 | [ComVisible(true)] 9 | public class GeneralOptions : BaseOptionPage { } 10 | } 11 | 12 | public class General : BaseOptionModel, IRatingConfig 13 | { 14 | [Category("General")] 15 | [DisplayName("Request timeout")] 16 | [Description("The number of seconds to allow the request to run before failing.")] 17 | [DefaultValue(20)] 18 | public int Timeout { get; set; } = 20; 19 | 20 | [Category("General")] 21 | [DisplayName("Enable validation")] 22 | [Description("Determines if error messages should be shown for unknown variables and incorrect URIs.")] 23 | [DefaultValue(true)] 24 | public bool EnableValidation { get; set; } = true; 25 | 26 | [Category("Response")] 27 | [DisplayName("Response Window Width")] 28 | [Description("The number of seconds to allow the request to run before failing.")] 29 | [Browsable(false)] 30 | [DefaultValue(500)] 31 | public int ResponseWindowWidth { get; set; } = 500; 32 | 33 | [Browsable(false)] 34 | public int RatingRequests { get; set; } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/RestClientVS/OutputWindow/ClassificationTypeDefinitions.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.Composition; 2 | using System.Windows.Media; 3 | using Microsoft.VisualStudio.Text.Classification; 4 | using Microsoft.VisualStudio.Utilities; 5 | 6 | namespace RestClientVS.OutputWindow 7 | { 8 | public class ClassificationTypeDefinitions 9 | { 10 | public const string StatusOk = "StatusOK"; 11 | public const string StatusBad = "StatusBad"; 12 | public const string HeaderName = "HeaderName"; 13 | 14 | [Export] 15 | [Name(StatusOk)] 16 | public static ClassificationTypeDefinition StatusOkDefinition { get; set; } 17 | 18 | [Name(StatusOk)] 19 | [UserVisible(false)] 20 | [Export(typeof(EditorFormatDefinition))] 21 | [ClassificationType(ClassificationTypeNames = StatusOk)] 22 | [Order(Before = Priority.Default)] 23 | public sealed class StatusOkFormat : ClassificationFormatDefinition 24 | { 25 | public StatusOkFormat() 26 | { 27 | ForegroundColor = Colors.Green; 28 | IsBold = true; 29 | } 30 | } 31 | 32 | [Export] 33 | [Name(StatusBad)] 34 | public static ClassificationTypeDefinition StatusBadDefinition { get; set; } 35 | 36 | [Name(StatusBad)] 37 | [UserVisible(false)] 38 | [Export(typeof(EditorFormatDefinition))] 39 | [ClassificationType(ClassificationTypeNames = StatusBad)] 40 | [Order(Before = Priority.Default)] 41 | public sealed class StatusBadFormat : ClassificationFormatDefinition 42 | { 43 | public StatusBadFormat() 44 | { 45 | ForegroundColor = Colors.Crimson; 46 | IsBold = true; 47 | } 48 | } 49 | 50 | [Export] 51 | [Name(HeaderName)] 52 | public static ClassificationTypeDefinition HeaderNameDefinition { get; set; } 53 | 54 | [Name(HeaderName)] 55 | [UserVisible(false)] 56 | [Export(typeof(EditorFormatDefinition))] 57 | [ClassificationType(ClassificationTypeNames = HeaderName)] 58 | [Order(Before = Priority.Default)] 59 | public sealed class HeaderNameFormat : ClassificationFormatDefinition 60 | { 61 | public HeaderNameFormat() 62 | { 63 | IsBold = true; 64 | } 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/RestClientVS/OutputWindow/OutputClassifier.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.ComponentModel.Composition; 3 | using System.Linq; 4 | using System.Text.RegularExpressions; 5 | using Microsoft.VisualStudio.Text; 6 | using Microsoft.VisualStudio.Text.Classification; 7 | using Microsoft.VisualStudio.Text.Tagging; 8 | using Microsoft.VisualStudio.Utilities; 9 | 10 | namespace RestClientVS.OutputWindow 11 | { 12 | [Export(typeof(ITaggerProvider))] 13 | [TagType(typeof(IClassificationTag))] 14 | [ContentType("output")] 15 | public class OutputClassifier : ITaggerProvider 16 | { 17 | [Import] internal IClassificationTypeRegistryService _classificationRegistry = null; 18 | 19 | public ITagger CreateTagger(ITextBuffer buffer) where T : ITag => 20 | buffer.Properties.GetOrCreateSingletonProperty(() => new OutputClassificationTagger(buffer, _classificationRegistry)) as ITagger; 21 | 22 | } 23 | 24 | internal class OutputClassificationTagger : ITagger 25 | { 26 | private readonly IClassificationType _headerNameType, _statusOkType, _statusBadType; 27 | private readonly ITextBuffer _buffer; 28 | private readonly Regex _status = new(@"^HTTP/\d.\d (?\d{3} .+)", RegexOptions.Compiled | RegexOptions.IgnoreCase); 29 | private readonly Regex _header = new(@"^(?[\w-]+):(?.+)", RegexOptions.Compiled | RegexOptions.IgnoreCase); 30 | 31 | internal OutputClassificationTagger(ITextBuffer buffer, IClassificationTypeRegistryService registry) 32 | { 33 | _headerNameType = registry.GetClassificationType(ClassificationTypeDefinitions.HeaderName); 34 | _statusOkType = registry.GetClassificationType(ClassificationTypeDefinitions.StatusOk); 35 | _statusBadType = registry.GetClassificationType(ClassificationTypeDefinitions.StatusBad); 36 | _buffer = buffer; 37 | } 38 | 39 | public IEnumerable> GetTags(NormalizedSnapshotSpanCollection spans) 40 | { 41 | if (spans.Count == 0 || 42 | !_buffer.CurrentSnapshot.Lines.Any() || 43 | !_buffer.CurrentSnapshot.Lines.First().GetText().StartsWith(Vsix.Name)) 44 | { 45 | yield return null; 46 | } 47 | 48 | foreach (SnapshotSpan span in spans) 49 | { 50 | var text = span.GetText(); 51 | 52 | Match match = _header.Match(text); 53 | 54 | if (match.Success) 55 | { 56 | Group nameGroup = match.Groups["name"]; 57 | var nameSpan = new SnapshotSpan(span.Snapshot, span.Start + nameGroup.Index, nameGroup.Length); 58 | var nameTag = new ClassificationTag(_headerNameType); 59 | yield return new TagSpan(nameSpan, nameTag); 60 | } 61 | else 62 | { 63 | match = _status.Match(text); 64 | 65 | if (match.Success) 66 | { 67 | Group statusGroup = match.Groups["status"]; 68 | var statusCode = int.Parse(statusGroup.Value[0].ToString()); 69 | IClassificationType type = statusCode == 2 || statusCode == 3 ? _statusOkType : _statusBadType; 70 | var statusTag = new ClassificationTag(type); 71 | yield return new TagSpan(span, statusTag); 72 | } 73 | } 74 | } 75 | } 76 | 77 | public event EventHandler TagsChanged 78 | { 79 | add { } 80 | remove { } 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/RestClientVS/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.InteropServices; 3 | using RestClientVS; 4 | 5 | [assembly: AssemblyTitle(Vsix.Name)] 6 | [assembly: AssemblyDescription(Vsix.Description)] 7 | [assembly: AssemblyConfiguration("")] 8 | [assembly: AssemblyCompany(Vsix.Author)] 9 | [assembly: AssemblyProduct(Vsix.Name)] 10 | [assembly: AssemblyCopyright(Vsix.Author)] 11 | [assembly: AssemblyTrademark("")] 12 | [assembly: AssemblyCulture("")] 13 | 14 | [assembly: ComVisible(false)] 15 | 16 | [assembly: AssemblyVersion(Vsix.Version)] 17 | [assembly: AssemblyFileVersion(Vsix.Version)] 18 | 19 | [assembly: ProvideCodeBase(AssemblyName = "RestClient")] 20 | 21 | namespace System.Runtime.CompilerServices 22 | { 23 | public class IsExternalInit { } 24 | } -------------------------------------------------------------------------------- /src/RestClientVS/Resources/Icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madskristensen/RestClientVS/ba6bdfc9a74011249d7713fcff72aa796b22450e/src/RestClientVS/Resources/Icon.png -------------------------------------------------------------------------------- /src/RestClientVS/RestClientVS.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) 5 | latest 6 | 7 | 8 | 9 | Debug 10 | AnyCPU 11 | 2.0 12 | {82b43b9b-a64c-4715-b499-d71e9ca2bd60};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} 13 | {3EFB5569-5699-4010-9173-27ED12EB6245} 14 | Library 15 | Properties 16 | RestClientVS 17 | RestClientVS 18 | v4.8 19 | true 20 | true 21 | true 22 | true 23 | false 24 | true 25 | true 26 | Program 27 | $(DevEnvDir)devenv.exe 28 | /rootsuffix Exp 29 | 30 | 31 | true 32 | full 33 | false 34 | bin\Debug\ 35 | DEBUG;TRACE 36 | prompt 37 | 4 38 | 39 | 40 | pdbonly 41 | true 42 | bin\Release\ 43 | TRACE 44 | prompt 45 | 4 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | ResponseControl.xaml 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | True 72 | True 73 | source.extension.vsixmanifest 74 | 75 | 76 | True 77 | True 78 | VSCommandTable.vsct 79 | 80 | 81 | 82 | 83 | Resources\LICENSE 84 | true 85 | 86 | 87 | Designer 88 | VsixManifestGenerator 89 | source.extension.cs 90 | 91 | 92 | PreserveNewest 93 | true 94 | 95 | 96 | 97 | 98 | Menus.ctmenu 99 | VsctGenerator 100 | VSCommandTable.cs 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | compile; build; native; contentfiles; analyzers; buildtransitive 118 | 119 | 120 | runtime; build; native; contentfiles; analyzers; buildtransitive 121 | all 122 | 123 | 124 | 125 | 126 | {2479064b-68e0-430e-bfeb-e2ecc670e019} 127 | RestClient 128 | 129 | 130 | 131 | 132 | Designer 133 | MSBuild:Compile 134 | 135 | 136 | 137 | 138 | 145 | -------------------------------------------------------------------------------- /src/RestClientVS/RestClientVSPackage.cs: -------------------------------------------------------------------------------- 1 | global using System; 2 | global using Community.VisualStudio.Toolkit; 3 | global using Microsoft.VisualStudio.Shell; 4 | global using Task = System.Threading.Tasks.Task; 5 | using System.Runtime.InteropServices; 6 | using System.Threading; 7 | using Microsoft.VisualStudio; 8 | using Microsoft.VisualStudio.Shell.Interop; 9 | 10 | namespace RestClientVS 11 | { 12 | [PackageRegistration(UseManagedResourcesOnly = true, AllowsBackgroundLoading = true)] 13 | [InstalledProductRegistration(Vsix.Name, Vsix.Description, Vsix.Version)] 14 | [ProvideMenuResource("Menus.ctmenu", 1)] 15 | [Guid(PackageGuids.RestClientVSString)] 16 | 17 | [ProvideLanguageService(typeof(LanguageFactory), LanguageFactory.LanguageName, 0, ShowHotURLs = false, DefaultToNonHotURLs = true)] 18 | [ProvideLanguageExtension(typeof(LanguageFactory), LanguageFactory.HttpFileExtension)] 19 | [ProvideLanguageExtension(typeof(LanguageFactory), LanguageFactory.RestFileExtension)] 20 | [ProvideLanguageEditorOptionPage(typeof(OptionsProvider.GeneralOptions), LanguageFactory.LanguageName, null, "Advanced", null, new[] { "http", "rest", "timeout" })] 21 | 22 | [ProvideEditorFactory(typeof(LanguageFactory), 351, CommonPhysicalViewAttributes = (int)__VSPHYSICALVIEWATTRIBUTES.PVA_SupportsPreview, TrustLevel = __VSEDITORTRUSTLEVEL.ETL_AlwaysTrusted)] 23 | [ProvideEditorExtension(typeof(LanguageFactory), LanguageFactory.HttpFileExtension, 65535, NameResourceID = 351)] 24 | [ProvideEditorExtension(typeof(LanguageFactory), LanguageFactory.RestFileExtension, 65535, NameResourceID = 351)] 25 | [ProvideEditorLogicalView(typeof(LanguageFactory), VSConstants.LOGVIEWID.TextView_string, IsTrusted = true)] 26 | 27 | [ProvideFileIcon(LanguageFactory.HttpFileExtension, "KnownMonikers.WebScript")] 28 | [ProvideFileIcon(LanguageFactory.RestFileExtension, "KnownMonikers.WebScript")] 29 | public sealed class RestClientVSPackage : ToolkitPackage 30 | { 31 | protected override async Task InitializeAsync(CancellationToken cancellationToken, IProgress progress) 32 | { 33 | await JoinableTaskFactory.SwitchToMainThreadAsync(); 34 | 35 | var language = new LanguageFactory(this); 36 | RegisterEditorFactory(language); 37 | language.RegisterLanguageService(this); 38 | 39 | await this.RegisterCommandsAsync(); 40 | await Commenting.InitializeAsync(); 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /src/RestClientVS/RestDocument.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Threading.Tasks; 3 | using Microsoft.VisualStudio.Text; 4 | using Microsoft.VisualStudio.Threading; 5 | using RestClient; 6 | 7 | namespace RestClientVS 8 | { 9 | public class RestDocument : Document, IDisposable 10 | { 11 | private readonly ITextBuffer _buffer; 12 | private bool _isDisposed; 13 | 14 | public string FileName { get; } 15 | public string ProjectName { get; private set; } 16 | 17 | public RestDocument(ITextBuffer buffer) 18 | : base(buffer.CurrentSnapshot.Lines.Select(line => line.GetTextIncludingLineBreak()).ToArray()) 19 | { 20 | _buffer = buffer; 21 | _buffer.Changed += BufferChanged; 22 | FileName = buffer.GetFileName(); 23 | 24 | General.Saved += OnSettingsSaved; 25 | 26 | ThreadHelper.JoinableTaskFactory.RunAsync(async () => 27 | { 28 | await Task.Yield(); 29 | Project project = await VS.Solutions.GetActiveProjectAsync(); 30 | ProjectName = project?.Name; 31 | }).FireAndForget(); 32 | } 33 | 34 | private void BufferChanged(object sender, TextContentChangedEventArgs e) 35 | { 36 | UpdateLines(_buffer.CurrentSnapshot.Lines.Select(line => line.GetTextIncludingLineBreak()).ToArray()); 37 | ParseAsync().FireAndForget(); 38 | } 39 | 40 | private async Task ParseAsync() 41 | { 42 | await TaskScheduler.Default; 43 | Parse(); 44 | } 45 | 46 | private void OnSettingsSaved(General obj) 47 | { 48 | ParseAsync().FireAndForget(); 49 | } 50 | 51 | public void Dispose() 52 | { 53 | if (!_isDisposed) 54 | { 55 | _buffer.Changed -= BufferChanged; 56 | General.Saved -= OnSettingsSaved; 57 | } 58 | 59 | _isDisposed = true; 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/RestClientVS/VSCommandTable.cs: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------------ 2 | // 3 | // This file was generated by VSIX Synchronizer 4 | // 5 | // ------------------------------------------------------------------------------ 6 | namespace RestClientVS 7 | { 8 | using System; 9 | 10 | /// 11 | /// Helper class that exposes all GUIDs used across VS Package. 12 | /// 13 | internal sealed partial class PackageGuids 14 | { 15 | public const string RestEditorFactoryString = "ed091b30-0d58-4343-9c24-4aad5c417422"; 16 | public static Guid RestEditorFactory = new Guid(RestEditorFactoryString); 17 | 18 | public const string RestClientVSString = "bb294b08-c71b-4feb-8772-5a10f5b8b36c"; 19 | public static Guid RestClientVS = new Guid(RestClientVSString); 20 | } 21 | /// 22 | /// Helper class that encapsulates all CommandIDs uses across VS Package. 23 | /// 24 | internal sealed partial class PackageIds 25 | { 26 | public const int RestCodeWin = 0x0001; 27 | public const int SendRequest = 0x0100; 28 | } 29 | } -------------------------------------------------------------------------------- /src/RestClientVS/VSCommandTable.vsct: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 18 | 19 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /src/RestClientVS/source.extension.cs: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------------ 2 | // 3 | // This file was generated by VSIX Synchronizer 4 | // 5 | // ------------------------------------------------------------------------------ 6 | namespace RestClientVS 7 | { 8 | internal sealed partial class Vsix 9 | { 10 | public const string Id = "RestClientVS.a7b4a362-3ce8-4953-9b19-a35166f2cbfd"; 11 | public const string Name = "REST Client"; 12 | public const string Description = @"REST Client allows you to send HTTP request and view the response in Visual Studio directly. Based on the popular VS Code extension"; 13 | public const string Language = "en-US"; 14 | public const string Version = "1.2"; 15 | public const string Author = "Mads Kristensen"; 16 | public const string Tags = "rest, http, postman"; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/RestClientVS/source.extension.vsixmanifest: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | REST Client 6 | REST Client allows you to send HTTP request and view the response in Visual Studio directly. Based on the popular VS Code extension 7 | https://github.com/madskristensen/RestClientVS 8 | Resources\LICENSE 9 | Resources\Icon.png 10 | Resources\Icon.png 11 | rest, http, postman 12 | true 13 | 14 | 15 | 16 | amd64 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /test/RestClientTest/Extensions.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using RestClient; 3 | 4 | namespace RestClientTest 5 | { 6 | public static class Extensions 7 | { 8 | public static async Task WaitForParsingCompleteAsync(this Document document) 9 | { 10 | while (document.IsParsing) 11 | { 12 | await Task.Delay(2); 13 | } 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /test/RestClientTest/HttpTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Net.Http; 4 | using System.Threading.Tasks; 5 | using RestClient; 6 | using RestClient.Client; 7 | using Xunit; 8 | 9 | namespace RestClientTest 10 | { 11 | public class HttpTest 12 | { 13 | [Theory] 14 | [InlineData("delete https://bing.com")] 15 | [InlineData("POST https://bing.com")] 16 | [InlineData("PUT https://api.github.com/users/madskristensen")] 17 | [InlineData("get https://api.github.com/users/madskristensen")] 18 | public async Task SendAsync(string url) 19 | { 20 | var doc = Document.FromLines(url); 21 | 22 | RequestResult client = await RequestSender.SendAsync(doc.Requests.First(), TimeSpan.FromSeconds(10)); 23 | var raw = await client.Response.ToRawStringAsync(); 24 | 25 | Assert.NotNull(client.Response); 26 | Assert.True(raw.Length > 50); 27 | } 28 | 29 | [Theory] 30 | [InlineData("application/json")] 31 | [InlineData("application/json; charset=utf-8")] 32 | public void AddHeadersParseContentTypeTest(string contentType) 33 | { 34 | var lines = new[] 35 | { 36 | "POST https://test.fake/api/users/add HTTP/1.1", 37 | "Content-type: " + contentType, 38 | "Accept: application/json", 39 | "", 40 | "{", 41 | "}" 42 | }; 43 | 44 | var doc = Document.FromLines(lines); 45 | 46 | Request request = doc.Requests?.FirstOrDefault(); 47 | 48 | HttpRequestMessage message = new (); 49 | 50 | RequestSender.AddHeaders(request, message); 51 | 52 | Assert.Equal(contentType, 53 | request.Headers.Where( 54 | h => h.Name.Text.IsTokenMatch("content-type")) 55 | .First().Value.Text.Trim()); 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /test/RestClientTest/RestClientTest.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net6.0 5 | false 6 | 7 | 8 | 9 | 10 | 11 | 12 | runtime; build; native; contentfiles; analyzers; buildtransitive 13 | all 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /test/RestClientTest/TokenTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using RestClient; 4 | using Xunit; 5 | 6 | namespace RestClientTest 7 | { 8 | public class TokenTest 9 | { 10 | [Theory] 11 | [InlineData(@"GET https://example.com")] 12 | [InlineData(@"post https://example.com?hat&ost")] 13 | [InlineData(@"options https://example.com")] 14 | [InlineData(@"Trace https://example.com?hat&ost")] 15 | [InlineData(@"Trace https://example.com?hat&ost HTTP/1.1")] 16 | public void OneLiners(string line) 17 | { 18 | var doc = Document.FromLines(line); 19 | ParseItem request = doc.Items?.First(); 20 | ParseItem method = doc.Items?.ElementAt(1); 21 | 22 | Assert.NotNull(method); 23 | Assert.Equal(ItemType.Request, request.Type); 24 | Assert.Equal(ItemType.Method, method.Type); 25 | Assert.Equal(0, method.Start); 26 | Assert.StartsWith(method.Text, line); 27 | } 28 | 29 | [Fact] 30 | public void RequestTextAfterLineBreak() 31 | { 32 | var lines = new[] { "\r", Environment.NewLine, @"GET https://example.com" }; 33 | 34 | var doc = Document.FromLines(lines); 35 | 36 | Request request = doc.Requests?.FirstOrDefault(); 37 | 38 | Assert.Equal("GET", request.Method.Text); 39 | Assert.Equal(3, request.Method.Start); 40 | } 41 | 42 | [Fact] 43 | public void RequestWithVersion() 44 | { 45 | var lines = new[] { "\r", Environment.NewLine, @"GET https://example.com http/1.1" }; 46 | 47 | var doc = Document.FromLines(lines); 48 | 49 | Request request = doc.Requests?.FirstOrDefault(); 50 | 51 | Assert.NotNull(request.Version); 52 | Assert.Equal("http/1.1", request.Version.Text); 53 | } 54 | 55 | [Fact] 56 | public void MultipleRequests() 57 | { 58 | var lines = new[] {@"get http://example.com\r\n", 59 | "\r\n", 60 | "###\r\n", 61 | "\r\n", 62 | "post http://bing.com"}; 63 | 64 | var doc = Document.FromLines(lines); 65 | 66 | Assert.Equal(2, doc.Requests.Count); 67 | } 68 | 69 | [Fact] 70 | public void RequestWithHeaderAndBody() 71 | { 72 | var lines = new[] 73 | { 74 | "GET https://example.com", 75 | "User-Agent: ost", 76 | "\r\n", 77 | "{\"enabled\": true}" 78 | }; 79 | 80 | var doc = Document.FromLines(lines); 81 | Request request = doc.Requests?.FirstOrDefault(); 82 | 83 | Assert.Single(doc.Requests); 84 | Assert.NotNull(request.Body); 85 | } 86 | 87 | [Fact] 88 | public void RequestWithHeaderAndMultilineBody() 89 | { 90 | var lines = new[] 91 | { 92 | "GET https://example.com", 93 | "User-Agent: ost", 94 | "\r\n", 95 | "{\r\n", 96 | "\"enabled\": true\r\n", 97 | "}" 98 | }; 99 | 100 | var doc = Document.FromLines(lines); 101 | Request request = doc.Requests.First(); 102 | 103 | Assert.NotNull(request.Body); 104 | Assert.Equal(21, request.Body.Length); 105 | } 106 | 107 | [Fact] 108 | public void RequestWithHeaderAndMultilineWwwFormBody() 109 | { 110 | var lines = new[] 111 | { 112 | "POST https://myserver/mypath/myoperation HTTP/1.1", 113 | "content-type: application/x-www-form-urlencoded; charset=utf-8", 114 | "\r\n", 115 | "f=json\r\n", 116 | "&inputLocations=123,45;123,46\r\n" 117 | }; 118 | 119 | var doc = Document.FromLines(lines); 120 | Request request = doc.Requests.First(); 121 | 122 | Assert.NotNull(request.Body); 123 | Assert.Equal("f=json&inputLocations=123,45;123,46", request.Body); 124 | } 125 | 126 | [Fact] 127 | public void RequestWithHeaderAndBodyAndComment() 128 | { 129 | var lines = new[] 130 | { 131 | "DELETE https://example.com", 132 | "User-Agent: ost", 133 | "#ost:hat", 134 | "\r\n", 135 | @"{ 136 | \t""enabled"": true 137 | }" 138 | }; 139 | 140 | var doc = Document.FromLines(lines); 141 | Request first = doc.Requests.FirstOrDefault(); 142 | 143 | Assert.NotNull(first); 144 | Assert.Single(doc.Requests); 145 | Assert.Equal(ItemType.HeaderName, first.Children.ElementAt(2).Type); 146 | Assert.Equal(ItemType.Body, first.Children.ElementAt(4).Type); 147 | Assert.Equal(@"{ 148 | \t""enabled"": true 149 | }", first.Body); 150 | } 151 | 152 | [Theory] 153 | [InlineData("")] 154 | [InlineData(" ")] 155 | [InlineData("\t\t")] 156 | [InlineData("\r")] 157 | [InlineData("\n")] 158 | [InlineData("\r\n")] 159 | public void EmptyLines(string line) 160 | { 161 | var doc = Document.FromLines(line); 162 | ParseItem first = doc.Items?.FirstOrDefault(); 163 | 164 | Assert.NotNull(first); 165 | Assert.Equal(ItemType.EmptyLine, first.Type); 166 | Assert.Equal(0, first.Start); 167 | Assert.Equal(line, first.Text); 168 | Assert.Equal(line.Length, first.Length); 169 | Assert.Equal(line.Length, first.End); 170 | } 171 | 172 | [Fact] 173 | public void BodyAfterComment() 174 | { 175 | var text = new[] { @"TraCe https://{{host}}/authors/{{name}}\r\n", 176 | "Content-Type: at{{contentType}}svin\r\n", 177 | "#ost\r\n", 178 | "mads: ost\r\n", 179 | "\r\n", 180 | "{\r\n", 181 | " \"content\": \"foo bar\",\r\n", 182 | " \"created_at\": \"{{createdAt}}\",\r\n", 183 | "\r\n", 184 | " \"modified_by\": \"$test$\"\r\n", 185 | "}\r\n", 186 | "\r\n", 187 | "\r\n",}; 188 | 189 | var doc = Document.FromLines(text); 190 | Request request = doc.Requests.First(); 191 | 192 | Assert.NotNull(request.Body); 193 | Assert.Contains("$test$", request.Body); 194 | Assert.EndsWith("}", request.Body.Trim()); 195 | } 196 | 197 | [Fact] 198 | public void VariableTokenization() 199 | { 200 | var text = $"@name = value"; 201 | 202 | var doc = Document.FromLines(text); 203 | ParseItem name = doc.Items.FirstOrDefault(); 204 | 205 | Assert.Equal(0, name.Start); 206 | Assert.Equal(5, name.Length); 207 | Assert.Equal(8, name.Next.Start); 208 | Assert.Equal(5, name.Next.Length); 209 | } 210 | 211 | [Fact] 212 | public void CommentInBetweenHeaders() 213 | { 214 | var text = new[] { @"POST https://example.com", 215 | "Content-Type:application/json", 216 | "#comment", 217 | "Accept: gzip" }; 218 | 219 | var doc = Document.FromLines(text); 220 | 221 | Assert.Equal(8, doc.Items.Count); 222 | } 223 | } 224 | } -------------------------------------------------------------------------------- /test/RestClientTest/VariableTest.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using RestClient; 3 | using Xunit; 4 | 5 | namespace RestClientTest 6 | { 7 | public class VariableTest 8 | { 9 | [Theory] 10 | [InlineData("@name=value", "value")] 11 | [InlineData("@name = value", "value")] 12 | [InlineData("@name= value", "value")] 13 | [InlineData("@name =value", "value")] 14 | [InlineData("@name\t=\t value", "value")] 15 | public void VariableDeclarations(string line, string value) 16 | { 17 | var doc = Document.FromLines(line); 18 | 19 | Variable first = doc.Variables?.FirstOrDefault(); 20 | 21 | Assert.NotNull(first); 22 | Assert.Equal(0, first.Name.Start); 23 | Assert.EndsWith(value, first.Value.Text); 24 | } 25 | 26 | [Theory] 27 | [InlineData("var1", "1")] 28 | public void ExpandUrlVariables(string name, string value) 29 | { 30 | var variable = $"@{name}={value}"; 31 | var request = "GET http://example.com?{{" + name + "}}"; 32 | 33 | var doc = Document.FromLines(variable, request); 34 | 35 | Request r = doc.Requests.FirstOrDefault(); 36 | 37 | Assert.Equal("GET http://example.com?" + value, r.ToString()); 38 | } 39 | 40 | [Fact] 41 | public void ExpandUrlVariablesRecursive() 42 | { 43 | var text = new[] { "@hostname=bing.com\r\n", 44 | "@host={{hostname}}\r\n", 45 | "GET https://{{host}}" }; 46 | 47 | var doc = Document.FromLines(text); 48 | 49 | Request r = doc.Requests.FirstOrDefault(); 50 | 51 | Assert.Equal("GET https://bing.com", r.ToString()); 52 | } 53 | 54 | [Fact] 55 | public void HeaderValueContainsVariable() 56 | { 57 | var text = new[] { "get http://ost.com\r\n", 58 | "name:{{hostname}}\r\n" }; 59 | 60 | var doc = Document.FromLines(text); 61 | 62 | Assert.True(doc.Requests.First().Headers.First().Value.References.Any()); 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /vs-publish.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/vsix-publish", 3 | "categories": [ "other", "coding" ], 4 | "identity": { 5 | "internalName": "RestClient" 6 | }, 7 | "assetFiles": [ 8 | { 9 | "pathOnDisk": "art/document.png", 10 | "targetPath": "art/document.png" 11 | }, 12 | { 13 | "pathOnDisk": "art/screenshot.png", 14 | "targetPath": "art/screenshot.png" 15 | }, 16 | { 17 | "pathOnDisk": "art/tooltip.png", 18 | "targetPath": "art/tooltip.png" 19 | }, 20 | { 21 | "pathOnDisk": "art/context-menu.png", 22 | "targetPath": "art/context-menu.png" 23 | }, 24 | { 25 | "pathOnDisk": "art/options.png", 26 | "targetPath": "art/options.png" 27 | } 28 | ], 29 | "overview": "README.md", 30 | "publisher": "MadsKristensen", 31 | "repo": "https://github.com/madskristensen/RestClientVS" 32 | } --------------------------------------------------------------------------------