Vscode merge (#4582)

* Merge from vscode 37cb23d3dd4f9433d56d4ba5ea3203580719a0bd

* fix issues with merges

* bump node version in azpipe

* replace license headers

* remove duplicate launch task

* fix build errors

* fix build errors

* fix tslint issues

* working through package and linux build issues

* more work

* wip

* fix packaged builds

* working through linux build errors

* wip

* wip

* wip

* fix mac and linux file limits

* iterate linux pipeline

* disable editor typing

* revert series to parallel

* remove optimize vscode from linux

* fix linting issues

* revert testing change

* add work round for new node

* readd packaging for extensions

* fix issue with angular not resolving decorator dependencies
This commit is contained in:
Anthony Dresser
2019-03-19 17:44:35 -07:00
committed by GitHub
parent 833d197412
commit 87765e8673
1879 changed files with 54505 additions and 38058 deletions

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="-1 0 16 16" enable-background="new -1 0 16 16"><path fill="#424242" d="M14 1v9h-1v-8h-8v-1h9zm-11 2v1h8v8h1v-9h-9zm7 2v9h-9v-9h9zm-2 2h-5v5h5v-5z"/><rect x="4" y="9" fill="#00539C" width="3" height="1"/></svg>

After

Width:  |  Height:  |  Size: 281 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="-1 0 16 16" enable-background="new -1 0 16 16"><path fill="#C5C5C5" d="M14 1v9h-1v-8h-8v-1h9zm-11 2v1h8v8h1v-9h-9zm7 2v9h-9v-9h9zm-2 2h-5v5h5v-5z"/><rect x="4" y="9" fill="#75BEFF" width="3" height="1"/></svg>

After

Width:  |  Height:  |  Size: 281 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path d="M13.451 5.609l-.579-.939-1.068.812-.076.094c-.335.415-.927 1.341-1.124 2.876l-.021.165.033.163.071.345c0 1.654-1.346 3-3 3-.795 0-1.545-.311-2.107-.868-.563-.567-.873-1.317-.873-2.111 0-1.431 1.007-2.632 2.351-2.929v2.926s2.528-2.087 2.984-2.461h.012l3.061-2.582-4.919-4.1h-1.137v2.404c-3.429.318-6.121 3.211-6.121 6.721 0 1.809.707 3.508 1.986 4.782 1.277 1.282 2.976 1.988 4.784 1.988 3.722 0 6.75-3.028 6.75-6.75 0-1.245-.349-2.468-1.007-3.536z" fill="#F6F6F6"/><path d="M12.6 6.134l-.094.071c-.269.333-.746 1.096-.91 2.375.057.277.092.495.092.545 0 2.206-1.794 4-4 4-1.098 0-2.093-.445-2.817-1.164-.718-.724-1.163-1.718-1.163-2.815 0-2.206 1.794-4 4-4l.351.025v1.85s1.626-1.342 1.631-1.339l1.869-1.577-3.5-2.917v2.218l-.371-.03c-3.176 0-5.75 2.574-5.75 5.75 0 1.593.648 3.034 1.695 4.076 1.042 1.046 2.482 1.694 4.076 1.694 3.176 0 5.75-2.574 5.75-5.75-.001-1.106-.318-2.135-.859-3.012z" fill="#424242"/></svg>

After

Width:  |  Height:  |  Size: 986 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path d="M13.451 5.609l-.579-.939-1.068.812-.076.094c-.335.415-.927 1.341-1.124 2.876l-.021.165.033.163.071.345c0 1.654-1.346 3-3 3-.795 0-1.545-.311-2.107-.868-.563-.567-.873-1.317-.873-2.111 0-1.431 1.007-2.632 2.351-2.929v2.926s2.528-2.087 2.984-2.461h.012l3.061-2.582-4.919-4.1h-1.137v2.404c-3.429.318-6.121 3.211-6.121 6.721 0 1.809.707 3.508 1.986 4.782 1.277 1.282 2.976 1.988 4.784 1.988 3.722 0 6.75-3.028 6.75-6.75 0-1.245-.349-2.468-1.007-3.536z" fill="#2D2D30"/><path d="M12.6 6.134l-.094.071c-.269.333-.746 1.096-.91 2.375.057.277.092.495.092.545 0 2.206-1.794 4-4 4-1.098 0-2.093-.445-2.817-1.164-.718-.724-1.163-1.718-1.163-2.815 0-2.206 1.794-4 4-4l.351.025v1.85s1.626-1.342 1.631-1.339l1.869-1.577-3.5-2.917v2.218l-.371-.03c-3.176 0-5.75 2.574-5.75 5.75 0 1.593.648 3.034 1.695 4.076 1.042 1.046 2.482 1.694 4.076 1.694 3.176 0 5.75-2.574 5.75-5.75-.001-1.106-.318-2.135-.859-3.012z" fill="#C5C5C5"/></svg>

After

Width:  |  Height:  |  Size: 986 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="3 3 16 16" enable-background="new 3 3 16 16"><polygon fill="#e8e8e8" points="12.597,11.042 15.4,13.845 13.844,15.4 11.042,12.598 8.239,15.4 6.683,13.845 9.485,11.042 6.683,8.239 8.238,6.683 11.042,9.486 13.845,6.683 15.4,8.239"/></svg>

After

Width:  |  Height:  |  Size: 307 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="3 3 16 16" enable-background="new 3 3 16 16"><polygon fill="#424242" points="12.597,11.042 15.4,13.845 13.844,15.4 11.042,12.598 8.239,15.4 6.683,13.845 9.485,11.042 6.683,8.239 8.238,6.683 11.042,9.486 13.845,6.683 15.4,8.239"/></svg>

After

Width:  |  Height:  |  Size: 307 B

View File

@@ -0,0 +1 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M7.065 13H15v2H2.056v-2h5.009zm3.661-12H7.385L8.44 2.061 7.505 3H15V1h-4.274zM3.237 9H2.056v2H15V9H3.237zm4.208-4l.995 1-.995 1H15V5H7.445z" fill="#C5C5C5"/><path d="M5.072 4.03L7.032 6 5.978 7.061l-1.96-1.97-1.961 1.97L1 6l1.96-1.97L1 2.061 2.056 1l1.96 1.97L5.977 1l1.057 1.061L5.072 4.03z" fill="#F48771"/></svg>

After

Width:  |  Height:  |  Size: 419 B

View File

@@ -0,0 +1 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M7.065 13H15v2H2.056v-2h5.009zm3.661-12H7.385L8.44 2.061 7.505 3H15V1h-4.274zM3.237 9H2.056v2H15V9H3.237zm4.208-4l.995 1-.995 1H15V5H7.445z" fill="#424242"/><path d="M5.072 4.03L7.032 6 5.978 7.061l-1.96-1.97-1.961 1.97L1 6l1.96-1.97L1 2.061 2.056 1l1.96 1.97L5.977 1l1.057 1.061L5.072 4.03z" fill="#A1260D"/></svg>

After

Width:  |  Height:  |  Size: 419 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.icon-canvas-transparent,.icon-vs-out{fill:#252526;}.icon-canvas-transparent{opacity:0;}.icon-vs-bg{fill:#c5c5c5;}</style></defs><title>Ellipsis_bold_16x</title><g id="canvas"><path class="icon-canvas-transparent" d="M16,0V16H0V0Z"/></g><g id="outline" style="display: none;"><path class="icon-vs-out" d="M6,7.5A2.5,2.5,0,1,1,3.5,5,2.5,2.5,0,0,1,6,7.5ZM8.5,5A2.5,2.5,0,1,0,11,7.5,2.5,2.5,0,0,0,8.5,5Zm5,0A2.5,2.5,0,1,0,16,7.5,2.5,2.5,0,0,0,13.5,5Z" style="display: none;"/></g><g id="iconBg"><path class="icon-vs-bg" d="M5,7.5A1.5,1.5,0,1,1,3.5,6,1.5,1.5,0,0,1,5,7.5ZM8.5,6A1.5,1.5,0,1,0,10,7.5,1.5,1.5,0,0,0,8.5,6Zm5,0A1.5,1.5,0,1,0,15,7.5,1.5,1.5,0,0,0,13.5,6Z"/></g></svg>

After

Width:  |  Height:  |  Size: 748 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.icon-canvas-transparent,.icon-vs-out{fill:#f6f6f6;}.icon-canvas-transparent{opacity:0;}.icon-vs-bg{fill:#424242;}</style></defs><title>Ellipsis_bold_16x</title><g id="canvas"><path class="icon-canvas-transparent" d="M16,0V16H0V0Z"/></g><g id="outline" style="display: none;"><path class="icon-vs-out" d="M6,7.5A2.5,2.5,0,1,1,3.5,5,2.5,2.5,0,0,1,6,7.5ZM8.5,5A2.5,2.5,0,1,0,11,7.5,2.5,2.5,0,0,0,8.5,5Zm5,0A2.5,2.5,0,1,0,16,7.5,2.5,2.5,0,0,0,13.5,5Z" style="display: none;"/></g><g id="iconBg"><path class="icon-vs-bg" d="M5,7.5A1.5,1.5,0,1,1,3.5,6,1.5,1.5,0,0,1,5,7.5ZM8.5,6A1.5,1.5,0,1,0,10,7.5,1.5,1.5,0,0,0,8.5,6Zm5,0A1.5,1.5,0,1,0,15,7.5,1.5,1.5,0,0,0,13.5,6Z"/></g></svg>

After

Width:  |  Height:  |  Size: 748 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><style>.icon-canvas-transparent{opacity:0;fill:#2d2d30}.icon-vs-out{fill:#2d2d30}.icon-vs-bg{fill:#c5c5c5}.icon-vs-fg{fill:#2b282e}</style><path class="icon-canvas-transparent" d="M16 16H0V0h16v16z" id="canvas"/><path class="icon-vs-out" d="M16 10.015l-.258.043c.155.455.258.935.258 1.442 0 2.481-2.019 4.5-4.5 4.5-.508 0-.988-.103-1.444-.259l-.043.259H5.986l-.373-2.237-1.846 1.317-2.848-2.847 1.319-1.847L0 10.013V5.986l2.238-.373L.919 3.767 3.768.919l1.846 1.319L5.986 0h4.028l.372 2.238L12.233.919l2.847 2.848-1.318 1.846L16 5.986v4.029z" id="outline" style="display: none;"/><path class="icon-vs-bg" d="M7 11.5c0-.501.101-.975.253-1.426A2.207 2.207 0 0 1 8 5.788c.958 0 1.767.613 2.074 1.466A4.417 4.417 0 0 1 11.5 7c1.421 0 2.675.675 3.5 1.706V6.834l-2.121-.354a5.14 5.14 0 0 0-.354-.854l1.25-1.75-1.65-1.65-1.752 1.251c-.137-.072-.262-.159-.408-.219-.147-.061-.296-.088-.444-.134L9.167 1H6.834L6.48 3.121c-.295.092-.581.21-.854.354l-1.75-1.25-1.65 1.65 1.252 1.752c-.073.137-.16.263-.221.409-.06.145-.087.295-.133.443L1 6.833v2.333l2.121.354c.092.295.21.581.354.854l-1.25 1.75 1.65 1.65 1.752-1.251c.137.072.262.159.408.219.146.06.296.087.444.133L6.833 15h1.873C7.675 14.175 7 12.921 7 11.5z" id="iconBg"/><path class="icon-vs-bg" d="M11.5 8a3.5 3.5 0 1 0 0 7 3.5 3.5 0 0 0 0-7zM9 12v-1h5v1H9z" id="notificationBg"/><g id="notificationFg"><path class="icon-vs-fg" d="M9 11h5v1H9z" style="display: none;"/></g></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><style>.icon-canvas-transparent{opacity:0;fill:#f6f6f6}.icon-vs-out{fill:#f6f6f6}.icon-vs-bg{fill:#424242}.icon-vs-fg{fill:#f0eff1}</style><path class="icon-canvas-transparent" d="M16 16H0V0h16v16z" id="canvas"/><path class="icon-vs-out" d="M16 10.015l-.258.043c.155.455.258.935.258 1.442 0 2.481-2.019 4.5-4.5 4.5-.508 0-.988-.103-1.444-.259l-.043.259H5.986l-.373-2.237-1.846 1.317-2.848-2.847 1.319-1.847L0 10.013V5.986l2.238-.373L.919 3.767 3.768.919l1.846 1.319L5.986 0h4.028l.372 2.238L12.233.919l2.847 2.848-1.318 1.846L16 5.986v4.029z" id="outline" style="display: none;"/><path class="icon-vs-bg" d="M7 11.5c0-.501.101-.975.253-1.426A2.207 2.207 0 0 1 8 5.788c.958 0 1.767.613 2.074 1.466A4.417 4.417 0 0 1 11.5 7c1.421 0 2.675.675 3.5 1.706V6.834l-2.121-.354a5.14 5.14 0 0 0-.354-.854l1.25-1.75-1.65-1.65-1.752 1.251c-.137-.072-.262-.159-.408-.219-.147-.061-.296-.088-.444-.134L9.167 1H6.834L6.48 3.121c-.295.092-.581.21-.854.354l-1.75-1.25-1.65 1.65 1.252 1.752c-.073.137-.16.263-.221.409-.06.145-.087.295-.133.443L1 6.833v2.333l2.121.354c.092.295.21.581.354.854l-1.25 1.75 1.65 1.65 1.752-1.251c.137.072.262.159.408.219.146.06.296.087.444.133L6.833 15h1.873C7.675 14.175 7 12.921 7 11.5z" id="iconBg"/><path class="icon-vs-bg" d="M11.5 8a3.5 3.5 0 1 0 0 7 3.5 3.5 0 0 0 0-7zM9 12v-1h5v1H9z" id="notificationBg"/><g id="notificationFg"><path class="icon-vs-fg" d="M9 11h5v1H9z" style="display: none;"/></g></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path fill="#e8e8e8" d="M6 4v8l4-4-4-4zm1 2.414l1.586 1.586-1.586 1.586v-3.172z"/></svg>

After

Width:  |  Height:  |  Size: 151 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path fill="#646465" d="M6 4v8l4-4-4-4zm1 2.414l1.586 1.586-1.586 1.586v-3.172z"/></svg>

After

Width:  |  Height:  |  Size: 151 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path fill="#e8e8e8" d="M11 10.07h-5.656l5.656-5.656v5.656z"/></svg>

After

Width:  |  Height:  |  Size: 131 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path fill="#646465" d="M11 10.07h-5.656l5.656-5.656v5.656z"/></svg>

After

Width:  |  Height:  |  Size: 131 B

View File

@@ -0,0 +1,11 @@
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="16px"
height="16px" viewBox="0 0 16 16" enable-background="new 0 0 16 16" xml:space="preserve">
<g id="icon_x5F_bg">
<path fill="#C5C5C5" d="M11,15V9H1v6H11z M2,14v-2h1v-1H2v-1h3v4H2z M10,11H8v2h2v1H7v-4h3V11z M3,13v-1h1v1H3z M13,7v6h-1V8H5V7
H13z M13,2V1h-1v5h3V2H13z M14,5h-1V3h1V5z M11,2v4H8V4h1v1h1V4H9V3H8V2H11z"/>
</g>
<g id="color_x5F_action">
<path fill="#75BEFF" d="M1.979,3.5L2,6L1,5v1.5L2.5,8L4,6.5V5L3,6L2.979,3.5c0-0.275,0.225-0.5,0.5-0.5H7V2H3.479
C2.651,2,1.979,2.673,1.979,3.5z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 637 B

View File

@@ -0,0 +1,11 @@
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="16px"
height="16px" viewBox="0 0 16 16" enable-background="new 0 0 16 16" xml:space="preserve">
<g id="icon_x5F_bg">
<path fill="#424242" d="M11,15V9H1v6H11z M2,14v-2h1v-1H2v-1h3v4H2z M10,11H8v2h2v1H7v-4h3V11z M3,13v-1h1v1H3z M13,7v6h-1V8H5V7
H13z M13,2V1h-1v5h3V2H13z M14,5h-1V3h1V5z M11,2v4H8V4h1v1h1V4H9V3H8V2H11z"/>
</g>
<g id="color_x5F_action">
<path fill="#00539C" d="M1.979,3.5L2,6L1,5v1.5L2.5,8L4,6.5V5L3,6L2.979,3.5c0-0.275,0.225-0.5,0.5-0.5H7V2H3.479
C2.651,2,1.979,2.673,1.979,3.5z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 637 B

View File

@@ -0,0 +1,13 @@
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="16px"
height="16px" viewBox="0 0 16 16" enable-background="new 0 0 16 16" xml:space="preserve">
<g id="icon_x5F_bg">
<g>
<path fill="#C5C5C5" d="M11,3V1h-1v5v1h1h2h1V4V3H11z M13,6h-2V4h2V6z"/>
<path fill="#C5C5C5" d="M2,15h7V9H2V15z M4,10h3v1H5v2h2v1H4V10z"/>
</g>
</g>
<g id="color_x5F_importance">
<path fill="#75BEFF" d="M3.979,3.5L4,6L3,5v1.5L4.5,8L6,6.5V5L5,6L4.979,3.5c0-0.275,0.225-0.5,0.5-0.5H9V2H5.479
C4.651,2,3.979,2.673,3.979,3.5z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 589 B

View File

@@ -0,0 +1,13 @@
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="16px"
height="16px" viewBox="0 0 16 16" enable-background="new 0 0 16 16" xml:space="preserve">
<g id="icon_x5F_bg">
<g>
<path fill="#424242" d="M11,3V1h-1v5v1h1h2h1V4V3H11z M13,6h-2V4h2V6z"/>
<path fill="#424242" d="M2,15h7V9H2V15z M4,10h3v1H5v2h2v1H4V10z"/>
</g>
</g>
<g id="color_x5F_importance">
<path fill="#00539C" d="M3.979,3.5L4,6L3,5v1.5L4.5,8L6,6.5V5L5,6L4.979,3.5c0-0.275,0.225-0.5,0.5-0.5H9V2H5.479
C4.651,2,3.979,2.673,3.979,3.5z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 589 B

View File

@@ -0,0 +1 @@
<svg fill="none" height="28" viewBox="0 0 28 28" width="28" xmlns="http://www.w3.org/2000/svg"><path d="m17.1249 2c-4.9127 0-8.89701 3.98533-8.89701 8.899 0 1.807.54686 3.4801 1.47014 4.8853 0 0-5.562 5.5346-7.20564 7.2056-1.644662 1.6701 1.0156 4.1304 2.63997 2.4442 1.62538-1.6832 7.10824-7.1072 7.10824-7.1072 1.4042.9243 3.0793 1.4711 4.8843 1.4711 4.9157 0 8.9-3.9873 8.9-8.899.001-4.91469-3.9843-8.899-8.9-8.899zm0 15.2544c-3.5095 0-6.3565-2.8449-6.3565-6.3554 0-3.51049 2.846-6.35643 6.3565-6.35643 3.5125 0 6.3574 2.84493 6.3574 6.35643 0 3.5105-2.8449 6.3554-6.3574 6.3554z" fill="#fff"/></svg>

After

Width:  |  Height:  |  Size: 603 B

View File

@@ -0,0 +1,9 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/* Activity Bar */
.monaco-workbench .activitybar .monaco-action-bar .action-label.search {
-webkit-mask: url('search-dark.svg') no-repeat 50% 50%;
}

View File

@@ -0,0 +1,398 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.search-view .search-widgets-container {
margin: 0px 9px 0 2px;
padding-top: 6px;
}
.search-view .search-widget .toggle-replace-button {
position: absolute;
top: 0;
left: 0;
width: 16px;
height: 100%;
box-sizing: border-box;
background-position: center center;
background-repeat: no-repeat;
cursor: pointer;
}
.search-view .search-widget .search-container,
.search-view .search-widget .replace-container {
margin-left: 17px;
}
.search-view .search-widget .monaco-inputbox > .wrapper {
height: 100%;
}
.search-view .search-widget .monaco-inputbox > .wrapper > .mirror,
.search-view .search-widget .monaco-inputbox > .wrapper > textarea.input {
padding: 3px;
padding-left: 4px;
}
.search-view .search-widget .monaco-inputbox > .wrapper > .mirror {
max-height: 134px;
}
.search-view .search-widget .monaco-inputbox > .wrapper > textarea.input {
overflow: initial;
height: 24px; /* set initial height before measure */
}
.search-view .monaco-inputbox > .wrapper > textarea.input::-webkit-scrollbar {
display: none;
}
.search-view .search-widget .monaco-findInput {
display: inline-block;
vertical-align: middle;
width: 100%;
}
.search-view .search-widget .replace-container {
margin-top: 6px;
position: relative;
display: inline-flex;
}
.search-view .search-widget .replace-container.disabled {
display: none;
}
.search-view .search-widget .replace-container .monaco-action-bar {
margin-left: 3px;
}
.search-view .search-widget .replace-container .monaco-action-bar {
height: 25px;
}
.search-view .search-widget .replace-container .monaco-action-bar .action-item .icon {
background-repeat: no-repeat;
width: 20px;
height: 25px;
}
.search-view .query-details {
min-height: 1em;
position: relative;
margin: 0 0 0 17px;
}
.search-view .query-details .more {
position: absolute;
margin-right: 0.3em;
right: 0;
cursor: pointer;
width: 16px;
height: 13px;
z-index: 2; /* Force it above the search results message, which has a negative top margin */
}
.hc-black .monaco-workbench .search-view .query-details .more,
.vs-dark .monaco-workbench .search-view .query-details .more {
background: url('ellipsis-inverse.svg') top center no-repeat;
}
.vs .monaco-workbench .search-view .query-details .more {
background: url('ellipsis.svg') top center no-repeat;
}
.search-view .query-details .file-types {
display: none;
}
.search-view .query-details .file-types > .monaco-inputbox {
width: 100%;
height: 25px;
}
.search-view .query-details.more .file-types {
display: inherit;
}
.search-view .query-details.more .file-types:last-child {
padding-bottom: 10px;
}
.search-view .query-details.more h4 {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
padding: 4px 0 0;
margin: 0;
font-size: 11px;
font-weight: normal;
}
.search-view .messages {
margin-top: -5px;
cursor: default;
}
.search-view .message {
padding-left: 22px;
padding-right: 22px;
padding-top: 0px;
}
.search-view .message p:first-child {
margin-top: 0px;
margin-bottom: 0px;
padding-bottom: 4px;
user-select: text;
}
.search-view .foldermatch,
.search-view .filematch {
display: flex;
position: relative;
line-height: 22px;
padding: 0;
}
.search-view:not(.wide) .foldermatch .monaco-icon-label,
.search-view:not(.wide) .filematch .monaco-icon-label {
flex: 1;
}
.search-view:not(.wide) .monaco-list .monaco-list-row:hover:not(.highlighted) .foldermatch .monaco-icon-label,
.search-view:not(.wide) .monaco-list .monaco-list-row.focused .foldermatch .monaco-icon-label,
.search-view:not(.wide) .monaco-list .monaco-list-row:hover:not(.highlighted) .filematch .monaco-icon-label,
.search-view:not(.wide) .monaco-list .monaco-list-row.focused .filematch .monaco-icon-label {
flex: 1;
}
.search-view.wide .foldermatch .badge,
.search-view.wide .filematch .badge {
margin-left: 10px;
}
.search-view .linematch {
position: relative;
line-height: 22px;
display: flex;
overflow: hidden;
}
.search-view .linematch > .match {
overflow: hidden;
text-overflow: ellipsis;
white-space: pre;
}
.search-view .linematch .matchLineNum {
margin-left: 7px;
margin-right: 4px;
opacity: .7;
font-size: 0.9em;
display: none;
}
.search-view .linematch .matchLineNum.show {
display: block;
}
.search-view.wide .monaco-list .monaco-list-row .foldermatch .actionBarContainer,
.search-view.wide .monaco-list .monaco-list-row .filematch .actionBarContainer,
.search-view .monaco-list .monaco-list-row .linematch .actionBarContainer {
flex: 1 0 auto;
}
.search-view:not(.wide) .monaco-list .monaco-list-row .foldermatch .actionBarContainer,
.search-view:not(.wide) .monaco-list .monaco-list-row .filematch .actionBarContainer {
flex: 0 0 auto;
}
.search-view.actions-right .monaco-list .monaco-list-row .foldermatch .actionBarContainer,
.search-view.actions-right .monaco-list .monaco-list-row .filematch .actionBarContainer,
.search-view.actions-right .monaco-list .monaco-list-row .linematch .actionBarContainer,
.search-view:not(.wide) .monaco-list .monaco-list-row .linematch .actionBarContainer {
text-align: right;
}
.search-view .monaco-list .monaco-list-row .monaco-action-bar {
line-height: 1em;
display: none;
padding: 0 0.8em 0 0.4em;
}
.search-view .monaco-list .monaco-list-row .monaco-action-bar .action-item {
margin: 0;
}
.search-view .monaco-list .monaco-list-row:hover:not(.highlighted) .monaco-action-bar,
.search-view .monaco-list .monaco-list-row.focused .monaco-action-bar {
display: inline-block;
}
.search-view .monaco-list .monaco-list-row .monaco-action-bar .action-label {
margin-right: 0.2em;
margin-top: 4px;
background-repeat: no-repeat;
width: 16px;
height: 16px;
}
/* Adjusts spacing in high contrast mode so that actions are vertically centered */
.hc-black .monaco-list .monaco-list-row .monaco-action-bar .action-label {
margin-top: 2px;
}
.search-view .action-remove {
background: url("action-remove.svg") center center no-repeat;
}
.search-view .action-replace {
background-image: url('replace.svg');
}
.search-view .action-replace-all {
background: url('replace-all.svg') center center no-repeat;
}
.hc-black .search-view .action-replace,
.vs-dark .search-view .action-replace {
background-image: url('replace-inverse.svg');
}
.hc-black .search-view .action-replace-all,
.vs-dark .search-view .action-replace-all {
background: url('replace-all-inverse.svg') center center no-repeat;
}
.search-view .monaco-count-badge {
margin-right: 12px;
}
.search-view:not(.wide) > .results > .monaco-list .monaco-list-row:hover .filematch .monaco-count-badge,
.search-view:not(.wide) > .results > .monaco-list .monaco-list-row:hover .foldermatch .monaco-count-badge,
.search-view:not(.wide) > .results > .monaco-list .monaco-list-row:hover .linematch .monaco-count-badge,
.search-view:not(.wide) > .results > .monaco-list .monaco-list-row.focused .filematch .monaco-count-badge,
.search-view:not(.wide) > .results > .monaco-list .monaco-list-row.focused .foldermatch .monaco-count-badge,
.search-view:not(.wide) > .results > .monaco-list .monaco-list-row.focused .linematch .monaco-count-badge {
display: none;
}
.monaco-workbench .search-action.refresh {
background: url('Refresh.svg') center center no-repeat;
}
.vs-dark .monaco-workbench .search-action.refresh,
.hc-black .monaco-workbench .search-action.refresh {
background: url('Refresh_inverse.svg') center center no-repeat;
}
.monaco-workbench .search-action.collapse {
background: url('CollapseAll.svg') center center no-repeat;
}
.vs-dark .monaco-workbench .search-action.collapse,
.hc-black .monaco-workbench .search-action.collapse {
background: url('CollapseAll_inverse.svg') center center no-repeat;
}
.monaco-workbench .search-action.clear-search-results {
background: url('clear-search-results.svg') center center no-repeat;
}
.vs-dark .monaco-workbench .search-action.clear-search-results,
.hc-black .monaco-workbench .search-action.clear-search-results {
background: url('clear-search-results-dark.svg') center center no-repeat;
}
.monaco-workbench .search-action.cancel-search {
background: url('stop.svg') center center no-repeat;
}
.vs-dark .monaco-workbench .search-action.cancel-search,
.hc-black .monaco-workbench .search-action.cancel-search {
background: url('stop-inverse.svg') center center no-repeat;
}
.vs .monaco-workbench .search-view .query-details .file-types .controls>.monaco-custom-checkbox.useExcludesAndIgnoreFiles {
background: url('excludeSettings.svg') center center no-repeat;
}
.vs-dark .monaco-workbench .search-view .query-details .file-types .controls>.monaco-custom-checkbox.useExcludesAndIgnoreFiles,
.hc-black .monaco-workbench .search-view .query-details .file-types .controls>.monaco-custom-checkbox.useExcludesAndIgnoreFiles {
background: url('excludeSettings-dark.svg') center center no-repeat;
}
.search-view .replace.findInFileMatch {
text-decoration: line-through;
}
.search-view .findInFileMatch,
.search-view .replaceMatch {
white-space: pre;
}
.hc-black .monaco-workbench .search-view .replaceMatch,
.hc-black .monaco-workbench .search-view .findInFileMatch {
background: none !important;
box-sizing: border-box;
}
.monaco-workbench .search-view a.prominent {
text-decoration: underline;
}
/* Theming */
.vs .search-view .search-widget .toggle-replace-button:hover {
background-color: rgba(0, 0, 0, 0.1) !important;
}
.vs-dark .search-view .search-widget .toggle-replace-button:hover {
background-color: rgba(255, 255, 255, 0.1) !important;
}
.vs .search-view .search-widget .toggle-replace-button.collapse {
background-image: url('expando-collapsed.svg');
}
.vs .search-view .search-widget .toggle-replace-button.expand {
background-image: url('expando-expanded.svg');
}
.vs-dark .search-view .action-remove,
.hc-black .search-view .action-remove {
background: url("action-remove-dark.svg") center center no-repeat;
}
.vs-dark .search-view .message {
opacity: .5;
}
.vs-dark .search-view .foldermatch,
.vs-dark .search-view .filematch {
padding: 0;
}
.vs-dark .search-view .search-widget .toggle-replace-button.expand,
.hc-black .search-view .search-widget .toggle-replace-button.expand {
background-image: url('expando-expanded-dark.svg');
}
.vs-dark .search-view .search-widget .toggle-replace-button.collapse,
.hc-black .search-view .search-widget .toggle-replace-button.collapse {
background-image: url('expando-collapsed-dark.svg');
}
/* High Contrast Theming */
.hc-black .monaco-workbench .search-view .foldermatch,
.hc-black .monaco-workbench .search-view .filematch,
.hc-black .monaco-workbench .search-view .linematch {
line-height: 20px;
}
.vs .panel .search-view .monaco-inputbox {
border: 1px solid #ddd;
}

View File

@@ -0,0 +1,3 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11 11H3V3H11V11Z" fill="#C5C5C5"/>
</svg>

After

Width:  |  Height:  |  Size: 147 B

View File

@@ -0,0 +1,3 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11 11H3V3H11V11Z" fill="#424242"/>
</svg>

After

Width:  |  Height:  |  Size: 147 B

View File

@@ -0,0 +1,249 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as arrays from 'vs/base/common/arrays';
import * as nls from 'vs/nls';
import { ThrottledDelayer } from 'vs/base/common/async';
import * as types from 'vs/base/common/types';
import { IAutoFocus } from 'vs/base/parts/quickopen/common/quickOpen';
import { QuickOpenEntry, QuickOpenModel, QuickOpenItemAccessor } from 'vs/base/parts/quickopen/browser/quickOpenModel';
import { QuickOpenHandler } from 'vs/workbench/browser/quickopen';
import { FileEntry, OpenFileHandler, FileQuickOpenModel } from 'vs/workbench/contrib/search/browser/openFileHandler';
import * as openSymbolHandler from 'vs/workbench/contrib/search/browser/openSymbolHandler';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IWorkbenchSearchConfiguration } from 'vs/workbench/contrib/search/common/search';
import { IRange } from 'vs/editor/common/core/range';
import { compareItemsByScore, scoreItem, ScorerCache, prepareQuery } from 'vs/base/parts/quickopen/common/quickOpenScorer';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { isPromiseCanceledError } from 'vs/base/common/errors';
import { CancellationToken } from 'vs/base/common/cancellation';
export import OpenSymbolHandler = openSymbolHandler.OpenSymbolHandler; // OpenSymbolHandler is used from an extension and must be in the main bundle file so it can load
interface ISearchWithRange {
search: string;
range: IRange;
}
export class OpenAnythingHandler extends QuickOpenHandler {
static readonly ID = 'workbench.picker.anything';
private static readonly LINE_COLON_PATTERN = /[#:\(](\d*)([#:,](\d*))?\)?$/;
private static readonly TYPING_SEARCH_DELAY = 200; // This delay accommodates for the user typing a word and then stops typing to start searching
private static readonly MAX_DISPLAYED_RESULTS = 512;
private openSymbolHandler: OpenSymbolHandler;
private openFileHandler: OpenFileHandler;
private searchDelayer: ThrottledDelayer<QuickOpenModel>;
private isClosed: boolean;
private scorerCache: ScorerCache;
private includeSymbols: boolean;
constructor(
@INotificationService private readonly notificationService: INotificationService,
@IInstantiationService instantiationService: IInstantiationService,
@IConfigurationService private readonly configurationService: IConfigurationService
) {
super();
this.scorerCache = Object.create(null);
this.searchDelayer = new ThrottledDelayer<QuickOpenModel>(OpenAnythingHandler.TYPING_SEARCH_DELAY);
this.openSymbolHandler = instantiationService.createInstance(OpenSymbolHandler);
this.openFileHandler = instantiationService.createInstance(OpenFileHandler);
this.updateHandlers(this.configurationService.getValue<IWorkbenchSearchConfiguration>());
this.registerListeners();
}
private registerListeners(): void {
this.configurationService.onDidChangeConfiguration(e => this.updateHandlers(this.configurationService.getValue<IWorkbenchSearchConfiguration>()));
}
private updateHandlers(configuration: IWorkbenchSearchConfiguration): void {
this.includeSymbols = configuration && configuration.search && configuration.search.quickOpen && configuration.search.quickOpen.includeSymbols;
// Files
this.openFileHandler.setOptions({
forceUseIcons: this.includeSymbols // only need icons for file results if we mix with symbol results
});
// Symbols
this.openSymbolHandler.setOptions({
skipDelay: true, // we have our own delay
skipLocalSymbols: true, // we only want global symbols
skipSorting: true // we sort combined with file results
});
}
getResults(searchValue: string, token: CancellationToken): Promise<QuickOpenModel> {
this.isClosed = false; // Treat this call as the handler being in use
// Find a suitable range from the pattern looking for ":" and "#"
const searchWithRange = this.extractRange(searchValue);
if (searchWithRange) {
searchValue = searchWithRange.search; // ignore range portion in query
}
// Prepare search for scoring
const query = prepareQuery(searchValue);
if (!query.value) {
return Promise.resolve(new QuickOpenModel()); // Respond directly to empty search
}
// The throttler needs a factory for its promises
const resultsPromise = () => {
const resultPromises: Promise<QuickOpenModel | FileQuickOpenModel>[] = [];
// File Results
const filePromise = this.openFileHandler.getResults(query.original, token, OpenAnythingHandler.MAX_DISPLAYED_RESULTS);
resultPromises.push(filePromise);
// Symbol Results (unless disabled or a range or absolute path is specified)
if (this.includeSymbols && !searchWithRange) {
resultPromises.push(this.openSymbolHandler.getResults(query.original, token));
}
// Join and sort unified
return Promise.all(resultPromises).then(results => {
// If the quick open widget has been closed meanwhile, ignore the result
if (this.isClosed || token.isCancellationRequested) {
return Promise.resolve<QuickOpenModel>(new QuickOpenModel());
}
// Combine results.
const mergedResults: QuickOpenEntry[] = ([] as QuickOpenEntry[]).concat(...results.map(r => r.entries));
// Sort
const compare = (elementA: QuickOpenEntry, elementB: QuickOpenEntry) => compareItemsByScore(elementA, elementB, query, true, QuickOpenItemAccessor, this.scorerCache);
const viewResults = arrays.top(mergedResults, compare, OpenAnythingHandler.MAX_DISPLAYED_RESULTS);
// Apply range and highlights to file entries
viewResults.forEach(entry => {
if (entry instanceof FileEntry) {
entry.setRange(searchWithRange ? searchWithRange.range : null);
const itemScore = scoreItem(entry, query, true, QuickOpenItemAccessor, this.scorerCache);
entry.setHighlights(itemScore.labelMatch || [], itemScore.descriptionMatch);
}
});
return Promise.resolve<QuickOpenModel>(new QuickOpenModel(viewResults));
}, error => {
if (!isPromiseCanceledError(error)) {
let message: Error | string;
if (error.message) {
message = error.message.replace(/[\*_\[\]]/g, '\\$&');
} else {
message = error;
}
this.notificationService.error(message);
}
return null;
});
};
// Trigger through delayer to prevent accumulation while the user is typing (except when expecting results to come from cache)
return this.hasShortResponseTime() ? resultsPromise() : this.searchDelayer.trigger(resultsPromise, OpenAnythingHandler.TYPING_SEARCH_DELAY);
}
hasShortResponseTime(): boolean {
if (!this.includeSymbols) {
return this.openFileHandler.hasShortResponseTime();
}
return this.openFileHandler.hasShortResponseTime() && this.openSymbolHandler.hasShortResponseTime();
}
private extractRange(value: string): ISearchWithRange | null {
if (!value) {
return null;
}
let range: IRange | null = null;
// Find Line/Column number from search value using RegExp
const patternMatch = OpenAnythingHandler.LINE_COLON_PATTERN.exec(value);
if (patternMatch && patternMatch.length > 1) {
const startLineNumber = parseInt(patternMatch[1], 10);
// Line Number
if (types.isNumber(startLineNumber)) {
range = {
startLineNumber: startLineNumber,
startColumn: 1,
endLineNumber: startLineNumber,
endColumn: 1
};
// Column Number
if (patternMatch.length > 3) {
const startColumn = parseInt(patternMatch[3], 10);
if (types.isNumber(startColumn)) {
range = {
startLineNumber: range.startLineNumber,
startColumn: startColumn,
endLineNumber: range.endLineNumber,
endColumn: startColumn
};
}
}
}
// User has typed "something:" or "something#" without a line number, in this case treat as start of file
else if (patternMatch[1] === '') {
range = {
startLineNumber: 1,
startColumn: 1,
endLineNumber: 1,
endColumn: 1
};
}
}
if (patternMatch && range) {
return {
search: value.substr(0, patternMatch.index), // clear range suffix from search value
range: range
};
}
return null;
}
getGroupLabel(): string {
return this.includeSymbols ? nls.localize('fileAndTypeResults', "file and symbol results") : nls.localize('fileResults', "file results");
}
getAutoFocus(searchValue: string): IAutoFocus {
return {
autoFocusFirstEntry: true
};
}
onOpen(): void {
this.openSymbolHandler.onOpen();
this.openFileHandler.onOpen();
}
onClose(canceled: boolean): void {
this.isClosed = true;
// Clear Cache
this.scorerCache = Object.create(null);
// Propagate
this.openSymbolHandler.onClose(canceled);
this.openFileHandler.onClose(canceled);
}
}

View File

@@ -0,0 +1,334 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as errors from 'vs/base/common/errors';
import * as nls from 'vs/nls';
import { isAbsolute } from 'vs/base/common/path';
import * as objects from 'vs/base/common/objects';
import { defaultGenerator } from 'vs/base/common/idGenerator';
import { URI } from 'vs/base/common/uri';
import { basename, dirname } from 'vs/base/common/resources';
import { IIconLabelValueOptions } from 'vs/base/browser/ui/iconLabel/iconLabel';
import { IModeService } from 'vs/editor/common/services/modeService';
import { getIconClasses } from 'vs/editor/common/services/getIconClasses';
import { IModelService } from 'vs/editor/common/services/modelService';
import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService';
import { IAutoFocus } from 'vs/base/parts/quickopen/common/quickOpen';
import { QuickOpenEntry, QuickOpenModel } from 'vs/base/parts/quickopen/browser/quickOpenModel';
import { QuickOpenHandler, EditorQuickOpenEntry } from 'vs/workbench/browser/quickopen';
import { QueryBuilder, IFileQueryBuilderOptions } from 'vs/workbench/contrib/search/common/queryBuilder';
import { EditorInput, IWorkbenchEditorConfiguration } from 'vs/workbench/common/editor';
import { IResourceInput } from 'vs/platform/editor/common/editor';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ISearchService, IFileSearchStats, IFileQuery, ISearchComplete } from 'vs/workbench/services/search/common/search';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { IRange } from 'vs/editor/common/core/range';
import { getOutOfWorkspaceEditorResources } from 'vs/workbench/contrib/search/common/search';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { prepareQuery, IPreparedQuery } from 'vs/base/parts/quickopen/common/quickOpenScorer';
import { IFileService } from 'vs/platform/files/common/files';
import { ILabelService } from 'vs/platform/label/common/label';
import { untildify } from 'vs/base/common/labels';
import { CancellationToken } from 'vs/base/common/cancellation';
export class FileQuickOpenModel extends QuickOpenModel {
constructor(entries: QuickOpenEntry[], stats?: IFileSearchStats) {
super(entries);
}
}
export class FileEntry extends EditorQuickOpenEntry {
private range: IRange | null;
constructor(
private resource: URI,
private name: string,
private description: string,
private icon: string,
@IEditorService editorService: IEditorService,
@IModeService private readonly modeService: IModeService,
@IModelService private readonly modelService: IModelService,
@IConfigurationService private readonly configurationService: IConfigurationService,
@IWorkspaceContextService contextService: IWorkspaceContextService
) {
super(editorService);
}
getLabel(): string {
return this.name;
}
getLabelOptions(): IIconLabelValueOptions {
return {
extraClasses: getIconClasses(this.modelService, this.modeService, this.resource)
};
}
getAriaLabel(): string {
return nls.localize('entryAriaLabel', "{0}, file picker", this.getLabel());
}
getDescription(): string {
return this.description;
}
getIcon(): string {
return this.icon;
}
getResource(): URI {
return this.resource;
}
setRange(range: IRange | null): void {
this.range = range;
}
mergeWithEditorHistory(): boolean {
return true;
}
getInput(): IResourceInput | EditorInput {
const input: IResourceInput = {
resource: this.resource,
options: {
pinned: !this.configurationService.getValue<IWorkbenchEditorConfiguration>().workbench.editor.enablePreviewFromQuickOpen,
selection: this.range ? this.range : undefined
}
};
return input;
}
}
export interface IOpenFileOptions {
forceUseIcons: boolean;
}
export class OpenFileHandler extends QuickOpenHandler {
private options: IOpenFileOptions;
private queryBuilder: QueryBuilder;
private cacheState: CacheState;
constructor(
@IEditorService private readonly editorService: IEditorService,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IWorkbenchThemeService private readonly themeService: IWorkbenchThemeService,
@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,
@ISearchService private readonly searchService: ISearchService,
@IEnvironmentService private readonly environmentService: IEnvironmentService,
@IFileService private readonly fileService: IFileService,
@ILabelService private readonly labelService: ILabelService
) {
super();
this.queryBuilder = this.instantiationService.createInstance(QueryBuilder);
}
setOptions(options: IOpenFileOptions) {
this.options = options;
}
getResults(searchValue: string, token: CancellationToken, maxSortedResults?: number): Promise<FileQuickOpenModel> {
const query = prepareQuery(searchValue);
// Respond directly to empty search
if (!query.value) {
return Promise.resolve(new FileQuickOpenModel([]));
}
// Untildify file pattern
query.value = untildify(query.value, this.environmentService.userHome);
// Do find results
return this.doFindResults(query, token, this.cacheState.cacheKey, maxSortedResults);
}
private doFindResults(query: IPreparedQuery, token: CancellationToken, cacheKey?: string, maxSortedResults?: number): Promise<FileQuickOpenModel> {
const queryOptions = this.doResolveQueryOptions(query, cacheKey, maxSortedResults);
let iconClass: string;
if (this.options && this.options.forceUseIcons && !this.themeService.getFileIconTheme()) {
iconClass = 'file'; // only use a generic file icon if we are forced to use an icon and have no icon theme set otherwise
}
return this.getAbsolutePathResult(query).then(result => {
if (token.isCancellationRequested) {
return Promise.resolve(<ISearchComplete>{ results: [] });
}
// If the original search value is an existing file on disk, return it immediately and bypass the search service
if (result) {
return Promise.resolve(<ISearchComplete>{ results: [{ resource: result }] });
}
return this.searchService.fileSearch(this.queryBuilder.file(this.contextService.getWorkspace().folders.map(folder => folder.uri), queryOptions), token);
}).then(complete => {
const results: QuickOpenEntry[] = [];
if (!token.isCancellationRequested) {
for (const fileMatch of complete.results) {
const label = basename(fileMatch.resource);
const description = this.labelService.getUriLabel(dirname(fileMatch.resource), { relative: true });
results.push(this.instantiationService.createInstance(FileEntry, fileMatch.resource, label, description, iconClass));
}
}
return new FileQuickOpenModel(results, <IFileSearchStats>complete.stats);
});
}
private getAbsolutePathResult(query: IPreparedQuery): Promise<URI | undefined> {
if (isAbsolute(query.original)) {
const resource = URI.file(query.original);
return this.fileService.resolveFile(resource).then(stat => stat.isDirectory ? undefined : resource, error => undefined);
}
return Promise.resolve(undefined);
}
private doResolveQueryOptions(query: IPreparedQuery, cacheKey?: string, maxSortedResults?: number): IFileQueryBuilderOptions {
const queryOptions: IFileQueryBuilderOptions = {
_reason: 'openFileHandler',
extraFileResources: getOutOfWorkspaceEditorResources(this.editorService, this.contextService),
filePattern: query.value,
cacheKey
};
if (typeof maxSortedResults === 'number') {
queryOptions.maxResults = maxSortedResults;
queryOptions.sortByScore = true;
}
return queryOptions;
}
hasShortResponseTime(): boolean {
return this.isCacheLoaded;
}
onOpen(): void {
this.cacheState = new CacheState(cacheKey => this.cacheQuery(cacheKey), query => this.searchService.fileSearch(query), cacheKey => this.searchService.clearCache(cacheKey), this.cacheState);
this.cacheState.load();
}
private cacheQuery(cacheKey: string): IFileQuery {
const options: IFileQueryBuilderOptions = {
_reason: 'openFileHandler',
extraFileResources: getOutOfWorkspaceEditorResources(this.editorService, this.contextService),
filePattern: '',
cacheKey: cacheKey,
maxResults: 0,
sortByScore: true,
};
const folderResources = this.contextService.getWorkspace().folders.map(folder => folder.uri);
const query = this.queryBuilder.file(folderResources, options);
return query;
}
get isCacheLoaded(): boolean {
return this.cacheState && this.cacheState.isLoaded;
}
getGroupLabel(): string {
return nls.localize('searchResults', "search results");
}
getAutoFocus(searchValue: string): IAutoFocus {
return {
autoFocusFirstEntry: true
};
}
}
enum LoadingPhase {
Created = 1,
Loading,
Loaded,
Errored,
Disposed
}
/**
* Exported for testing.
*/
export class CacheState {
private _cacheKey = defaultGenerator.nextId();
private query: IFileQuery;
private loadingPhase = LoadingPhase.Created;
private promise: Promise<void>;
constructor(cacheQuery: (cacheKey: string) => IFileQuery, private doLoad: (query: IFileQuery) => Promise<any>, private doDispose: (cacheKey: string) => Promise<void>, private previous: CacheState | null) {
this.query = cacheQuery(this._cacheKey);
if (this.previous) {
const current = objects.assign({}, this.query, { cacheKey: null });
const previous = objects.assign({}, this.previous.query, { cacheKey: null });
if (!objects.equals(current, previous)) {
this.previous.dispose();
this.previous = null;
}
}
}
get cacheKey(): string {
return this.loadingPhase === LoadingPhase.Loaded || !this.previous ? this._cacheKey : this.previous.cacheKey;
}
get isLoaded(): boolean {
const isLoaded = this.loadingPhase === LoadingPhase.Loaded;
return isLoaded || !this.previous ? isLoaded : this.previous.isLoaded;
}
get isUpdating(): boolean {
const isUpdating = this.loadingPhase === LoadingPhase.Loading;
return isUpdating || !this.previous ? isUpdating : this.previous.isUpdating;
}
load(): void {
if (this.isUpdating) {
return;
}
this.loadingPhase = LoadingPhase.Loading;
this.promise = this.doLoad(this.query)
.then(() => {
this.loadingPhase = LoadingPhase.Loaded;
if (this.previous) {
this.previous.dispose();
this.previous = null;
}
}, err => {
this.loadingPhase = LoadingPhase.Errored;
errors.onUnexpectedError(err);
});
}
dispose(): void {
if (this.promise) {
this.promise.then(undefined, () => { })
.then(() => {
this.loadingPhase = LoadingPhase.Disposed;
return this.doDispose(this._cacheKey);
}).then(undefined, err => {
errors.onUnexpectedError(err);
});
} else {
this.loadingPhase = LoadingPhase.Disposed;
}
if (this.previous) {
this.previous.dispose();
this.previous = null;
}
}
}

View File

@@ -0,0 +1,231 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as nls from 'vs/nls';
import { URI } from 'vs/base/common/uri';
import { onUnexpectedError } from 'vs/base/common/errors';
import { ThrottledDelayer } from 'vs/base/common/async';
import { QuickOpenHandler, EditorQuickOpenEntry } from 'vs/workbench/browser/quickopen';
import { QuickOpenModel, QuickOpenEntry, compareEntries } from 'vs/base/parts/quickopen/browser/quickOpenModel';
import { IAutoFocus, Mode, IEntryRunContext } from 'vs/base/parts/quickopen/common/quickOpen';
import * as filters from 'vs/base/common/filters';
import * as strings from 'vs/base/common/strings';
import { Range } from 'vs/editor/common/core/range';
import { IWorkbenchEditorConfiguration } from 'vs/workbench/common/editor';
import { symbolKindToCssClass } from 'vs/editor/common/modes';
import { IResourceInput } from 'vs/platform/editor/common/editor';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IWorkspaceSymbolProvider, getWorkspaceSymbols, IWorkspaceSymbol } from 'vs/workbench/contrib/search/common/search';
import { basename } from 'vs/base/common/resources';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { ILabelService } from 'vs/platform/label/common/label';
import { CancellationToken } from 'vs/base/common/cancellation';
import { Schemas } from 'vs/base/common/network';
import { IOpenerService } from 'vs/platform/opener/common/opener';
class SymbolEntry extends EditorQuickOpenEntry {
private bearingResolve: Promise<this | undefined>;
constructor(
private bearing: IWorkspaceSymbol,
private provider: IWorkspaceSymbolProvider,
@IConfigurationService private readonly configurationService: IConfigurationService,
@IEditorService editorService: IEditorService,
@ILabelService private readonly labelService: ILabelService,
@IOpenerService private readonly openerService: IOpenerService
) {
super(editorService);
}
getLabel(): string {
return this.bearing.name;
}
getAriaLabel(): string {
return nls.localize('entryAriaLabel', "{0}, symbols picker", this.getLabel());
}
getDescription(): string | null {
const containerName = this.bearing.containerName;
if (this.bearing.location.uri) {
if (containerName) {
return `${containerName}${basename(this.bearing.location.uri)}`;
}
return this.labelService.getUriLabel(this.bearing.location.uri, { relative: true });
}
return containerName || null;
}
getIcon(): string {
return symbolKindToCssClass(this.bearing.kind);
}
getResource(): URI {
return this.bearing.location.uri;
}
run(mode: Mode, context: IEntryRunContext): boolean {
// resolve this type bearing if neccessary
if (!this.bearingResolve && typeof this.provider.resolveWorkspaceSymbol === 'function' && !this.bearing.location.range) {
this.bearingResolve = Promise.resolve(this.provider.resolveWorkspaceSymbol(this.bearing, CancellationToken.None)).then(result => {
this.bearing = result || this.bearing;
return this;
}, onUnexpectedError);
}
// open after resolving
Promise.resolve(this.bearingResolve).then(() => {
const scheme = this.bearing.location.uri ? this.bearing.location.uri.scheme : undefined;
if (scheme === Schemas.http || scheme === Schemas.https) {
if (mode === Mode.OPEN || mode === Mode.OPEN_IN_BACKGROUND) {
this.openerService.open(this.bearing.location.uri); // support http/https resources (https://github.com/Microsoft/vscode/issues/58924))
}
} else {
super.run(mode, context);
}
});
// hide if OPEN
return mode === Mode.OPEN;
}
getInput(): IResourceInput {
const input: IResourceInput = {
resource: this.bearing.location.uri,
options: {
pinned: !this.configurationService.getValue<IWorkbenchEditorConfiguration>().workbench.editor.enablePreviewFromQuickOpen
}
};
if (this.bearing.location.range) {
input.options!.selection = Range.collapseToStart(this.bearing.location.range);
}
return input;
}
static compare(elementA: SymbolEntry, elementB: SymbolEntry, searchValue: string): number {
// Sort by Type if name is identical
const elementAName = elementA.getLabel().toLowerCase();
const elementBName = elementB.getLabel().toLowerCase();
if (elementAName === elementBName) {
let elementAType = symbolKindToCssClass(elementA.bearing.kind);
let elementBType = symbolKindToCssClass(elementB.bearing.kind);
return elementAType.localeCompare(elementBType);
}
return compareEntries(elementA, elementB, searchValue);
}
}
export interface IOpenSymbolOptions {
skipSorting: boolean;
skipLocalSymbols: boolean;
skipDelay: boolean;
}
export class OpenSymbolHandler extends QuickOpenHandler {
static readonly ID = 'workbench.picker.symbols';
private static readonly TYPING_SEARCH_DELAY = 200; // This delay accommodates for the user typing a word and then stops typing to start searching
private delayer: ThrottledDelayer<QuickOpenEntry[]>;
private options: IOpenSymbolOptions;
constructor(@IInstantiationService private readonly instantiationService: IInstantiationService) {
super();
this.delayer = new ThrottledDelayer<QuickOpenEntry[]>(OpenSymbolHandler.TYPING_SEARCH_DELAY);
this.options = Object.create(null);
}
setOptions(options: IOpenSymbolOptions) {
this.options = options;
}
canRun(): boolean | string {
return true;
}
getResults(searchValue: string, token: CancellationToken): Promise<QuickOpenModel> {
searchValue = searchValue.trim();
let promise: Promise<QuickOpenEntry[]>;
if (!this.options.skipDelay) {
promise = this.delayer.trigger(() => {
if (token.isCancellationRequested) {
return Promise.resolve([]);
}
return this.doGetResults(searchValue, token);
});
} else {
promise = this.doGetResults(searchValue, token);
}
return promise.then(e => new QuickOpenModel(e));
}
private doGetResults(searchValue: string, token: CancellationToken): Promise<SymbolEntry[]> {
return getWorkspaceSymbols(searchValue, token).then(tuples => {
if (token.isCancellationRequested) {
return [];
}
const result: SymbolEntry[] = [];
for (let tuple of tuples) {
const [provider, bearings] = tuple;
this.fillInSymbolEntries(result, provider, bearings, searchValue);
}
// Sort (Standalone only)
if (!this.options.skipSorting) {
searchValue = searchValue ? strings.stripWildcards(searchValue.toLowerCase()) : searchValue;
return result.sort((a, b) => SymbolEntry.compare(a, b, searchValue));
}
return result;
});
}
private fillInSymbolEntries(bucket: SymbolEntry[], provider: IWorkspaceSymbolProvider, types: IWorkspaceSymbol[], searchValue: string): void {
// Convert to Entries
for (let element of types) {
if (this.options.skipLocalSymbols && !!element.containerName) {
continue; // ignore local symbols if we are told so
}
const entry = this.instantiationService.createInstance(SymbolEntry, element, provider);
entry.setHighlights(filters.matchesFuzzy2(searchValue, entry.getLabel()) || []);
bucket.push(entry);
}
}
getGroupLabel(): string {
return nls.localize('symbols', "symbol results");
}
getEmptyLabel(searchString: string): string {
if (searchString.length > 0) {
return nls.localize('noSymbolsMatching', "No symbols matching");
}
return nls.localize('noSymbolsWithoutInput', "Type to search for symbols");
}
getAutoFocus(searchValue: string): IAutoFocus {
return {
autoFocusFirstEntry: true,
autoFocusPrefixMatch: searchValue.trim()
};
}
}

View File

@@ -0,0 +1,212 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as nls from 'vs/nls';
import * as dom from 'vs/base/browser/dom';
import { Widget } from 'vs/base/browser/ui/widget';
import { Checkbox } from 'vs/base/browser/ui/checkbox/checkbox';
import { IContextViewProvider } from 'vs/base/browser/ui/contextview/contextview';
import { IInputValidator, HistoryInputBox } from 'vs/base/browser/ui/inputbox/inputBox';
import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { KeyCode } from 'vs/base/common/keyCodes';
import { Event as CommonEvent, Emitter } from 'vs/base/common/event';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { attachInputBoxStyler, attachCheckboxStyler } from 'vs/platform/theme/common/styler';
import { ContextScopedHistoryInputBox } from 'vs/platform/browser/contextScopedHistoryWidget';
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
export interface IOptions {
placeholder?: string;
width?: number;
validation?: IInputValidator;
ariaLabel?: string;
history?: string[];
}
export class PatternInputWidget extends Widget {
static OPTION_CHANGE: string = 'optionChange';
inputFocusTracker: dom.IFocusTracker;
private width: number;
private placeholder: string;
private ariaLabel: string;
private domNode: HTMLElement;
protected inputBox: HistoryInputBox;
private _onSubmit = this._register(new Emitter<boolean>());
onSubmit: CommonEvent<boolean> = this._onSubmit.event;
private _onCancel = this._register(new Emitter<boolean>());
onCancel: CommonEvent<boolean> = this._onCancel.event;
constructor(parent: HTMLElement, private contextViewProvider: IContextViewProvider, options: IOptions = Object.create(null),
@IThemeService protected themeService: IThemeService,
@IContextKeyService private readonly contextKeyService: IContextKeyService
) {
super();
this.width = options.width || 100;
this.placeholder = options.placeholder || '';
this.ariaLabel = options.ariaLabel || nls.localize('defaultLabel', "input");
this.render(options);
parent.appendChild(this.domNode);
}
dispose(): void {
super.dispose();
if (this.inputFocusTracker) {
this.inputFocusTracker.dispose();
}
}
setWidth(newWidth: number): void {
this.width = newWidth;
this.domNode.style.width = this.width + 'px';
this.contextViewProvider.layout();
this.setInputWidth();
}
getValue(): string {
return this.inputBox.value;
}
setValue(value: string): void {
if (this.inputBox.value !== value) {
this.inputBox.value = value;
}
}
select(): void {
this.inputBox.select();
}
focus(): void {
this.inputBox.focus();
}
inputHasFocus(): boolean {
return this.inputBox.hasFocus();
}
private setInputWidth(): void {
this.inputBox.width = this.width - this.getSubcontrolsWidth() - 2; // 2 for input box border
}
protected getSubcontrolsWidth(): number {
return 0;
}
getHistory(): string[] {
return this.inputBox.getHistory();
}
clearHistory(): void {
this.inputBox.clearHistory();
}
onSearchSubmit(): void {
this.inputBox.addToHistory();
}
showNextTerm() {
this.inputBox.showNextValue();
}
showPreviousTerm() {
this.inputBox.showPreviousValue();
}
private render(options: IOptions): void {
this.domNode = document.createElement('div');
this.domNode.style.width = this.width + 'px';
dom.addClass(this.domNode, 'monaco-findInput');
this.inputBox = new ContextScopedHistoryInputBox(this.domNode, this.contextViewProvider, {
placeholder: this.placeholder || '',
ariaLabel: this.ariaLabel || '',
validationOptions: {
validation: undefined
},
history: options.history || []
}, this.contextKeyService);
this._register(attachInputBoxStyler(this.inputBox, this.themeService));
this.inputFocusTracker = dom.trackFocus(this.inputBox.inputElement);
this.onkeyup(this.inputBox.inputElement, (keyboardEvent) => this.onInputKeyUp(keyboardEvent));
const controls = document.createElement('div');
controls.className = 'controls';
this.renderSubcontrols(controls);
this.domNode.appendChild(controls);
this.setInputWidth();
}
protected renderSubcontrols(controlsDiv: HTMLDivElement): void {
}
private onInputKeyUp(keyboardEvent: IKeyboardEvent) {
switch (keyboardEvent.keyCode) {
case KeyCode.Enter:
this._onSubmit.fire(false);
return;
case KeyCode.Escape:
this._onCancel.fire(false);
return;
default:
return;
}
}
}
export class ExcludePatternInputWidget extends PatternInputWidget {
constructor(parent: HTMLElement, contextViewProvider: IContextViewProvider, options: IOptions = Object.create(null),
@IThemeService themeService: IThemeService,
@IContextKeyService contextKeyService: IContextKeyService
) {
super(parent, contextViewProvider, options, themeService, contextKeyService);
}
private useExcludesAndIgnoreFilesBox: Checkbox;
dispose(): void {
super.dispose();
this.useExcludesAndIgnoreFilesBox.dispose();
}
useExcludesAndIgnoreFiles(): boolean {
return this.useExcludesAndIgnoreFilesBox.checked;
}
setUseExcludesAndIgnoreFiles(value: boolean) {
this.useExcludesAndIgnoreFilesBox.checked = value;
}
protected getSubcontrolsWidth(): number {
return super.getSubcontrolsWidth() + this.useExcludesAndIgnoreFilesBox.width();
}
protected renderSubcontrols(controlsDiv: HTMLDivElement): void {
this.useExcludesAndIgnoreFilesBox = this._register(new Checkbox({
actionClassName: 'useExcludesAndIgnoreFiles',
title: nls.localize('useExcludesAndIgnoreFilesDescription', "Use Exclude Settings and Ignore Files"),
isChecked: true,
}));
this._register(this.useExcludesAndIgnoreFilesBox.onChange(viaKeyboard => {
if (!viaKeyboard) {
this.inputBox.focus();
}
}));
this._register(attachCheckboxStyler(this.useExcludesAndIgnoreFilesBox, this.themeService));
controlsDiv.appendChild(this.useExcludesAndIgnoreFilesBox.domNode);
super.renderSubcontrols(controlsDiv);
}
}

View File

@@ -0,0 +1,15 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { IReplaceService } from 'vs/workbench/contrib/search/common/replace';
import { ReplaceService, ReplacePreviewContentProvider } from 'vs/workbench/contrib/search/browser/replaceService';
import { Registry } from 'vs/platform/registry/common/platform';
import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions';
import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle';
export function registerContributions(): void {
registerSingleton(IReplaceService, ReplaceService, true);
Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench).registerWorkbenchContribution(ReplacePreviewContentProvider, LifecyclePhase.Starting);
}

View File

@@ -0,0 +1,213 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as nls from 'vs/nls';
import * as errors from 'vs/base/common/errors';
import { URI } from 'vs/base/common/uri';
import * as network from 'vs/base/common/network';
import { Disposable } from 'vs/base/common/lifecycle';
import { IReplaceService } from 'vs/workbench/contrib/search/common/replace';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { IModelService } from 'vs/editor/common/services/modelService';
import { IModeService } from 'vs/editor/common/services/modeService';
import { Match, FileMatch, FileMatchOrMatch, ISearchWorkbenchService } from 'vs/workbench/contrib/search/common/searchModel';
import { IProgressRunner } from 'vs/platform/progress/common/progress';
import { ITextModelService, ITextModelContentProvider } from 'vs/editor/common/services/resolverService';
import { IWorkbenchContribution } from 'vs/workbench/common/contributions';
import { ScrollType } from 'vs/editor/common/editorCommon';
import { ITextModel, IIdentifiedSingleEditOperation } from 'vs/editor/common/model';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ResourceTextEdit } from 'vs/editor/common/modes';
import { createTextBufferFactoryFromSnapshot } from 'vs/editor/common/model/textModel';
import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService';
import { Range } from 'vs/editor/common/core/range';
import { EditOperation } from 'vs/editor/common/core/editOperation';
import { mergeSort } from 'vs/base/common/arrays';
const REPLACE_PREVIEW = 'replacePreview';
const toReplaceResource = (fileResource: URI): URI => {
return fileResource.with({ scheme: network.Schemas.internal, fragment: REPLACE_PREVIEW, query: JSON.stringify({ scheme: fileResource.scheme }) });
};
const toFileResource = (replaceResource: URI): URI => {
return replaceResource.with({ scheme: JSON.parse(replaceResource.query)['scheme'], fragment: '', query: '' });
};
export class ReplacePreviewContentProvider implements ITextModelContentProvider, IWorkbenchContribution {
constructor(
@IInstantiationService private readonly instantiationService: IInstantiationService,
@ITextModelService private readonly textModelResolverService: ITextModelService
) {
this.textModelResolverService.registerTextModelContentProvider(network.Schemas.internal, this);
}
provideTextContent(uri: URI): Promise<ITextModel> | null {
if (uri.fragment === REPLACE_PREVIEW) {
return this.instantiationService.createInstance(ReplacePreviewModel).resolve(uri);
}
return null;
}
}
class ReplacePreviewModel extends Disposable {
constructor(
@IModelService private readonly modelService: IModelService,
@IModeService private readonly modeService: IModeService,
@ITextModelService private readonly textModelResolverService: ITextModelService,
@IReplaceService private readonly replaceService: IReplaceService,
@ISearchWorkbenchService private readonly searchWorkbenchService: ISearchWorkbenchService
) {
super();
}
resolve(replacePreviewUri: URI): Promise<ITextModel> {
const fileResource = toFileResource(replacePreviewUri);
const fileMatch = <FileMatch>this.searchWorkbenchService.searchModel.searchResult.matches().filter(match => match.resource().toString() === fileResource.toString())[0];
return this.textModelResolverService.createModelReference(fileResource).then(ref => {
ref = this._register(ref);
const sourceModel = ref.object.textEditorModel;
const sourceModelModeId = sourceModel.getLanguageIdentifier().language;
const replacePreviewModel = this.modelService.createModel(createTextBufferFactoryFromSnapshot(sourceModel.createSnapshot()), this.modeService.create(sourceModelModeId), replacePreviewUri);
this._register(fileMatch.onChange(modelChange => this.update(sourceModel, replacePreviewModel, fileMatch, modelChange)));
this._register(this.searchWorkbenchService.searchModel.onReplaceTermChanged(() => this.update(sourceModel, replacePreviewModel, fileMatch)));
this._register(fileMatch.onDispose(() => replacePreviewModel.dispose())); // TODO@Sandeep we should not dispose a model directly but rather the reference (depends on https://github.com/Microsoft/vscode/issues/17073)
this._register(replacePreviewModel.onWillDispose(() => this.dispose()));
this._register(sourceModel.onWillDispose(() => this.dispose()));
return replacePreviewModel;
});
}
private update(sourceModel: ITextModel, replacePreviewModel: ITextModel, fileMatch: FileMatch, override: boolean = false): void {
if (!sourceModel.isDisposed() && !replacePreviewModel.isDisposed()) {
this.replaceService.updateReplacePreview(fileMatch, override);
}
}
}
export class ReplaceService implements IReplaceService {
_serviceBrand: any;
constructor(
@ITextFileService private readonly textFileService: ITextFileService,
@IEditorService private readonly editorService: IEditorService,
@ITextModelService private readonly textModelResolverService: ITextModelService,
@IBulkEditService private readonly bulkEditorService: IBulkEditService
) { }
replace(match: Match): Promise<any>;
replace(files: FileMatch[], progress?: IProgressRunner): Promise<any>;
replace(match: FileMatchOrMatch, progress?: IProgressRunner, resource?: URI): Promise<any>;
replace(arg: any, progress: IProgressRunner | undefined = undefined, resource: URI | null = null): Promise<any> {
const edits: ResourceTextEdit[] = this.createEdits(arg, resource);
return this.bulkEditorService.apply({ edits }, { progress }).then(() => this.textFileService.saveAll(edits.map(e => e.resource)));
}
openReplacePreview(element: FileMatchOrMatch, preserveFocus?: boolean, sideBySide?: boolean, pinned?: boolean): Promise<any> {
const fileMatch = element instanceof Match ? element.parent() : element;
return this.editorService.openEditor({
leftResource: fileMatch.resource(),
rightResource: toReplaceResource(fileMatch.resource()),
label: nls.localize('fileReplaceChanges', "{0} ↔ {1} (Replace Preview)", fileMatch.name(), fileMatch.name()),
options: {
preserveFocus,
pinned,
revealIfVisible: true
}
}).then(editor => {
const disposable = fileMatch.onDispose(() => {
if (editor && editor.input) {
editor.input.dispose();
}
disposable.dispose();
});
this.updateReplacePreview(fileMatch).then(() => {
if (editor) {
const editorControl = editor.getControl();
if (element instanceof Match) {
editorControl.revealLineInCenter(element.range().startLineNumber, ScrollType.Immediate);
}
}
});
}, errors.onUnexpectedError);
}
updateReplacePreview(fileMatch: FileMatch, override: boolean = false): Promise<void> {
const replacePreviewUri = toReplaceResource(fileMatch.resource());
return Promise.all([this.textModelResolverService.createModelReference(fileMatch.resource()), this.textModelResolverService.createModelReference(replacePreviewUri)])
.then(([sourceModelRef, replaceModelRef]) => {
const sourceModel = sourceModelRef.object.textEditorModel;
const replaceModel = replaceModelRef.object.textEditorModel;
const returnValue = Promise.resolve(null);
// If model is disposed do not update
if (sourceModel && replaceModel) {
if (override) {
replaceModel.setValue(sourceModel.getValue());
} else {
replaceModel.undo();
}
this.applyEditsToPreview(fileMatch, replaceModel);
}
return returnValue.then(() => {
sourceModelRef.dispose();
replaceModelRef.dispose();
});
});
}
private applyEditsToPreview(fileMatch: FileMatch, replaceModel: ITextModel): void {
const resourceEdits = this.createEdits(fileMatch, replaceModel.uri);
const modelEdits: IIdentifiedSingleEditOperation[] = [];
for (const resourceEdit of resourceEdits) {
for (const edit of resourceEdit.edits) {
const range = Range.lift(edit.range);
modelEdits.push(EditOperation.replaceMove(range, edit.text));
}
}
replaceModel.pushEditOperations([], mergeSort(modelEdits, (a, b) => Range.compareRangesUsingStarts(a.range, b.range)), () => []);
}
private createEdits(arg: FileMatchOrMatch | FileMatch[], resource: URI | null = null): ResourceTextEdit[] {
const edits: ResourceTextEdit[] = [];
if (arg instanceof Match) {
const match = <Match>arg;
edits.push(this.createEdit(match, match.replaceString, resource));
}
if (arg instanceof FileMatch) {
arg = [arg];
}
if (arg instanceof Array) {
arg.forEach(element => {
const fileMatch = <FileMatch>element;
if (fileMatch.count() > 0) {
edits.push(...fileMatch.matches().map(match => this.createEdit(match, match.replaceString, resource)));
}
});
}
return edits;
}
private createEdit(match: Match, text: string, resource: URI | null = null): ResourceTextEdit {
const fileMatch: FileMatch = match.parent();
const resourceEdit: ResourceTextEdit = {
resource: resource !== null ? resource : fileMatch.resource(),
edits: [{
range: match.range(),
text: text
}]
};
return resourceEdit;
}
}

View File

@@ -0,0 +1,774 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./media/search.contribution';
import { Action } from 'vs/base/common/actions';
import { distinct } from 'vs/base/common/arrays';
import { illegalArgument } from 'vs/base/common/errors';
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
import { Schemas } from 'vs/base/common/network';
import * as objects from 'vs/base/common/objects';
import * as platform from 'vs/base/common/platform';
import { dirname } from 'vs/base/common/resources';
import { URI } from 'vs/base/common/uri';
import { registerLanguageCommand } from 'vs/editor/browser/editorExtensions';
import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService';
import { getSelectionSearchString } from 'vs/editor/contrib/find/findController';
import { ToggleCaseSensitiveKeybinding, ToggleRegexKeybinding, ToggleWholeWordKeybinding } from 'vs/editor/contrib/find/findModel';
import * as nls from 'vs/nls';
import { ICommandAction, MenuId, MenuRegistry, SyncActionDescriptor } from 'vs/platform/actions/common/actions';
import { CommandsRegistry, ICommandHandler } from 'vs/platform/commands/common/commands';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { ConfigurationScope, Extensions as ConfigurationExtensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry';
import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
import { IFileService } from 'vs/platform/files/common/files';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle';
import { IListService, WorkbenchListFocusContextKey, WorkbenchObjectTree } from 'vs/platform/list/browser/listService';
import { IQuickOpenService } from 'vs/platform/quickOpen/common/quickOpen';
import { Registry } from 'vs/platform/registry/common/platform';
import { ISearchConfigurationProperties, ISearchConfiguration, VIEWLET_ID, PANEL_ID, VIEW_ID, VIEW_CONTAINER } from 'vs/workbench/services/search/common/search';
import { defaultQuickOpenContextKey } from 'vs/workbench/browser/parts/quickopen/quickopen';
import { Extensions as QuickOpenExtensions, IQuickOpenRegistry, QuickOpenHandlerDescriptor } from 'vs/workbench/browser/quickopen';
import { Extensions as ActionExtensions, IWorkbenchActionRegistry } from 'vs/workbench/common/actions';
import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry, IWorkbenchContribution } from 'vs/workbench/common/contributions';
import { ResourceContextKey } from 'vs/workbench/common/resources';
import { getMultiSelectedResources } from 'vs/workbench/contrib/files/browser/files';
import { ExplorerFolderContext, ExplorerRootContext, FilesExplorerFocusCondition } from 'vs/workbench/contrib/files/common/files';
import { OpenAnythingHandler } from 'vs/workbench/contrib/search/browser/openAnythingHandler';
import { OpenSymbolHandler } from 'vs/workbench/contrib/search/browser/openSymbolHandler';
import { registerContributions as replaceContributions } from 'vs/workbench/contrib/search/browser/replaceContributions';
import { clearHistoryCommand, ClearSearchResultsAction, CloseReplaceAction, CollapseDeepestExpandedLevelAction, copyAllCommand, copyMatchCommand, copyPathCommand, FindInFilesAction, FocusNextInputAction, FocusNextSearchResultAction, FocusPreviousInputAction, FocusPreviousSearchResultAction, focusSearchListCommand, getSearchView, openSearchView, OpenSearchViewletAction, RefreshAction, RemoveAction, ReplaceAction, ReplaceAllAction, ReplaceAllInFolderAction, ReplaceInFilesAction, toggleCaseSensitiveCommand, toggleRegexCommand, toggleWholeWordCommand } from 'vs/workbench/contrib/search/browser/searchActions';
import { registerContributions as searchWidgetContributions } from 'vs/workbench/contrib/search/browser/searchWidget';
import * as Constants from 'vs/workbench/contrib/search/common/constants';
import { getWorkspaceSymbols } from 'vs/workbench/contrib/search/common/search';
import { FileMatchOrMatch, ISearchWorkbenchService, RenderableMatch, SearchWorkbenchService } from 'vs/workbench/contrib/search/common/searchModel';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { IPanelService } from 'vs/workbench/services/panel/common/panelService';
import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet';
import { PanelRegistry, Extensions as PanelExtensions, PanelDescriptor } from 'vs/workbench/browser/panel';
import { ViewletDescriptor, ViewletRegistry, Extensions as ViewletExtensions } from 'vs/workbench/browser/viewlet';
import { ISearchHistoryService, SearchHistoryService } from 'vs/workbench/contrib/search/common/searchHistoryService';
import { SearchViewlet } from 'vs/workbench/contrib/search/browser/searchViewlet';
import { SearchPanel } from 'vs/workbench/contrib/search/browser/searchPanel';
import { IViewsRegistry, Extensions as ViewExtensions } from 'vs/workbench/common/views';
import { SearchView } from 'vs/workbench/contrib/search/browser/searchView';
registerSingleton(ISearchWorkbenchService, SearchWorkbenchService, true);
registerSingleton(ISearchHistoryService, SearchHistoryService, true);
replaceContributions();
searchWidgetContributions();
const category = nls.localize('search', "Search");
KeybindingsRegistry.registerCommandAndKeybindingRule({
id: 'workbench.action.search.toggleQueryDetails',
weight: KeybindingWeight.WorkbenchContrib,
when: Constants.SearchViewVisibleKey,
primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_J,
handler: accessor => {
const searchView = getSearchView(accessor.get(IViewletService), accessor.get(IPanelService));
if (searchView) {
searchView.toggleQueryDetails();
}
}
});
KeybindingsRegistry.registerCommandAndKeybindingRule({
id: Constants.FocusSearchFromResults,
weight: KeybindingWeight.WorkbenchContrib,
when: ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.FirstMatchFocusKey),
primary: KeyMod.CtrlCmd | KeyCode.UpArrow,
handler: (accessor, args: any) => {
const searchView = getSearchView(accessor.get(IViewletService), accessor.get(IPanelService));
if (searchView) {
searchView.focusPreviousInputBox();
}
}
});
KeybindingsRegistry.registerCommandAndKeybindingRule({
id: Constants.OpenMatchToSide,
weight: KeybindingWeight.WorkbenchContrib,
when: ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.FileMatchOrMatchFocusKey),
primary: KeyMod.CtrlCmd | KeyCode.Enter,
mac: {
primary: KeyMod.WinCtrl | KeyCode.Enter
},
handler: (accessor, args: any) => {
const searchView = getSearchView(accessor.get(IViewletService), accessor.get(IPanelService));
if (searchView) {
const tree: WorkbenchObjectTree<RenderableMatch> = searchView.getControl();
searchView.open(<FileMatchOrMatch>tree.getFocus()[0], false, true, true);
}
}
});
KeybindingsRegistry.registerCommandAndKeybindingRule({
id: Constants.CancelActionId,
weight: KeybindingWeight.WorkbenchContrib,
when: ContextKeyExpr.and(Constants.SearchViewVisibleKey, WorkbenchListFocusContextKey),
primary: KeyCode.Escape,
handler: (accessor, args: any) => {
const searchView = getSearchView(accessor.get(IViewletService), accessor.get(IPanelService));
if (searchView) {
searchView.cancelSearch();
}
}
});
KeybindingsRegistry.registerCommandAndKeybindingRule({
id: Constants.RemoveActionId,
weight: KeybindingWeight.WorkbenchContrib,
when: ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.FileMatchOrMatchFocusKey),
primary: KeyCode.Delete,
mac: {
primary: KeyMod.CtrlCmd | KeyCode.Backspace,
},
handler: (accessor, args: any) => {
const searchView = getSearchView(accessor.get(IViewletService), accessor.get(IPanelService));
if (searchView) {
const tree: WorkbenchObjectTree<RenderableMatch> = searchView.getControl();
accessor.get(IInstantiationService).createInstance(RemoveAction, tree, tree.getFocus()[0]).run();
}
}
});
KeybindingsRegistry.registerCommandAndKeybindingRule({
id: Constants.ReplaceActionId,
weight: KeybindingWeight.WorkbenchContrib,
when: ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.ReplaceActiveKey, Constants.MatchFocusKey),
primary: KeyMod.Shift | KeyMod.CtrlCmd | KeyCode.KEY_1,
handler: (accessor, args: any) => {
const searchView = getSearchView(accessor.get(IViewletService), accessor.get(IPanelService));
if (searchView) {
const tree: WorkbenchObjectTree<RenderableMatch> = searchView.getControl();
accessor.get(IInstantiationService).createInstance(ReplaceAction, tree, tree.getFocus()[0], searchView).run();
}
}
});
KeybindingsRegistry.registerCommandAndKeybindingRule({
id: Constants.ReplaceAllInFileActionId,
weight: KeybindingWeight.WorkbenchContrib,
when: ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.ReplaceActiveKey, Constants.FileFocusKey),
primary: KeyMod.Shift | KeyMod.CtrlCmd | KeyCode.KEY_1,
secondary: [KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Enter],
handler: (accessor, args: any) => {
const searchView = getSearchView(accessor.get(IViewletService), accessor.get(IPanelService));
if (searchView) {
const tree: WorkbenchObjectTree<RenderableMatch> = searchView.getControl();
accessor.get(IInstantiationService).createInstance(ReplaceAllAction, searchView, tree.getFocus()[0]).run();
}
}
});
KeybindingsRegistry.registerCommandAndKeybindingRule({
id: Constants.ReplaceAllInFolderActionId,
weight: KeybindingWeight.WorkbenchContrib,
when: ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.ReplaceActiveKey, Constants.FolderFocusKey),
primary: KeyMod.Shift | KeyMod.CtrlCmd | KeyCode.KEY_1,
secondary: [KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Enter],
handler: (accessor, args: any) => {
const searchView = getSearchView(accessor.get(IViewletService), accessor.get(IPanelService));
if (searchView) {
const tree: WorkbenchObjectTree<RenderableMatch> = searchView.getControl();
accessor.get(IInstantiationService).createInstance(ReplaceAllInFolderAction, tree, tree.getFocus()[0]).run();
}
}
});
KeybindingsRegistry.registerCommandAndKeybindingRule({
id: Constants.CloseReplaceWidgetActionId,
weight: KeybindingWeight.WorkbenchContrib,
when: ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.ReplaceInputBoxFocusedKey),
primary: KeyCode.Escape,
handler: (accessor, args: any) => {
accessor.get(IInstantiationService).createInstance(CloseReplaceAction, Constants.CloseReplaceWidgetActionId, '').run();
}
});
KeybindingsRegistry.registerCommandAndKeybindingRule({
id: FocusNextInputAction.ID,
weight: KeybindingWeight.WorkbenchContrib,
when: ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.InputBoxFocusedKey),
primary: KeyMod.CtrlCmd | KeyCode.DownArrow,
handler: (accessor, args: any) => {
accessor.get(IInstantiationService).createInstance(FocusNextInputAction, FocusNextInputAction.ID, '').run();
}
});
KeybindingsRegistry.registerCommandAndKeybindingRule({
id: FocusPreviousInputAction.ID,
weight: KeybindingWeight.WorkbenchContrib,
when: ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.InputBoxFocusedKey, Constants.SearchInputBoxFocusedKey.toNegated()),
primary: KeyMod.CtrlCmd | KeyCode.UpArrow,
handler: (accessor, args: any) => {
accessor.get(IInstantiationService).createInstance(FocusPreviousInputAction, FocusPreviousInputAction.ID, '').run();
}
});
MenuRegistry.appendMenuItem(MenuId.SearchContext, {
command: {
id: Constants.ReplaceActionId,
title: ReplaceAction.LABEL
},
when: ContextKeyExpr.and(Constants.ReplaceActiveKey, Constants.MatchFocusKey),
group: 'search',
order: 1
});
MenuRegistry.appendMenuItem(MenuId.SearchContext, {
command: {
id: Constants.ReplaceAllInFolderActionId,
title: ReplaceAllInFolderAction.LABEL
},
when: ContextKeyExpr.and(Constants.ReplaceActiveKey, Constants.FolderFocusKey),
group: 'search',
order: 1
});
MenuRegistry.appendMenuItem(MenuId.SearchContext, {
command: {
id: Constants.ReplaceAllInFileActionId,
title: ReplaceAllAction.LABEL
},
when: ContextKeyExpr.and(Constants.ReplaceActiveKey, Constants.FileFocusKey),
group: 'search',
order: 1
});
MenuRegistry.appendMenuItem(MenuId.SearchContext, {
command: {
id: Constants.RemoveActionId,
title: RemoveAction.LABEL
},
when: Constants.FileMatchOrMatchFocusKey,
group: 'search',
order: 2
});
KeybindingsRegistry.registerCommandAndKeybindingRule({
id: Constants.CopyMatchCommandId,
weight: KeybindingWeight.WorkbenchContrib,
when: Constants.FileMatchOrMatchFocusKey,
primary: KeyMod.CtrlCmd | KeyCode.KEY_C,
handler: copyMatchCommand
});
MenuRegistry.appendMenuItem(MenuId.SearchContext, {
command: {
id: Constants.CopyMatchCommandId,
title: nls.localize('copyMatchLabel', "Copy")
},
when: Constants.FileMatchOrMatchFocusKey,
group: 'search_2',
order: 1
});
KeybindingsRegistry.registerCommandAndKeybindingRule({
id: Constants.CopyPathCommandId,
weight: KeybindingWeight.WorkbenchContrib,
when: Constants.FileMatchOrFolderMatchFocusKey,
primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KEY_C,
win: {
primary: KeyMod.Shift | KeyMod.Alt | KeyCode.KEY_C
},
handler: copyPathCommand
});
MenuRegistry.appendMenuItem(MenuId.SearchContext, {
command: {
id: Constants.CopyPathCommandId,
title: nls.localize('copyPathLabel', "Copy Path")
},
when: Constants.FileMatchOrFolderMatchFocusKey,
group: 'search_2',
order: 2
});
MenuRegistry.appendMenuItem(MenuId.SearchContext, {
command: {
id: Constants.CopyAllCommandId,
title: nls.localize('copyAllLabel', "Copy All")
},
when: Constants.HasSearchResults,
group: 'search_2',
order: 3
});
CommandsRegistry.registerCommand({
id: Constants.CopyAllCommandId,
handler: copyAllCommand
});
CommandsRegistry.registerCommand({
id: Constants.ClearSearchHistoryCommandId,
handler: clearHistoryCommand
});
const clearSearchHistoryLabel = nls.localize('clearSearchHistoryLabel', "Clear Search History");
const ClearSearchHistoryCommand: ICommandAction = {
id: Constants.ClearSearchHistoryCommandId,
title: clearSearchHistoryLabel,
category
};
MenuRegistry.addCommand(ClearSearchHistoryCommand);
CommandsRegistry.registerCommand({
id: Constants.ToggleSearchViewPositionCommandId,
handler: (accessor) => {
const configurationService = accessor.get(IConfigurationService);
const currentValue = configurationService.getValue<ISearchConfigurationProperties>('search').location;
const toggleValue = currentValue === 'sidebar' ? 'panel' : 'sidebar';
configurationService.updateValue('search.location', toggleValue);
}
});
const toggleSearchViewPositionLabel = nls.localize('toggleSearchViewPositionLabel', "Toggle Search View Position");
const ToggleSearchViewPositionCommand: ICommandAction = {
id: Constants.ToggleSearchViewPositionCommandId,
title: toggleSearchViewPositionLabel,
category
};
MenuRegistry.addCommand(ToggleSearchViewPositionCommand);
MenuRegistry.appendMenuItem(MenuId.SearchContext, {
command: ToggleSearchViewPositionCommand,
when: Constants.SearchViewVisibleKey,
group: 'search_9',
order: 1
});
CommandsRegistry.registerCommand({
id: Constants.FocusSearchListCommandID,
handler: focusSearchListCommand
});
const focusSearchListCommandLabel = nls.localize('focusSearchListCommandLabel', "Focus List");
const FocusSearchListCommand: ICommandAction = {
id: Constants.FocusSearchListCommandID,
title: focusSearchListCommandLabel,
category
};
MenuRegistry.addCommand(FocusSearchListCommand);
const searchInFolderCommand: ICommandHandler = (accessor, resource?: URI) => {
const listService = accessor.get(IListService);
const viewletService = accessor.get(IViewletService);
const panelService = accessor.get(IPanelService);
const fileService = accessor.get(IFileService);
const configurationService = accessor.get(IConfigurationService);
const resources = getMultiSelectedResources(resource, listService, accessor.get(IEditorService));
return openSearchView(viewletService, panelService, configurationService, true).then(searchView => {
if (resources && resources.length) {
return fileService.resolveFiles(resources.map(resource => ({ resource }))).then(results => {
const folders: URI[] = [];
results.forEach(result => {
if (result.success) {
folders.push(result.stat.isDirectory ? result.stat.resource : dirname(result.stat.resource));
}
});
searchView.searchInFolders(distinct(folders, folder => folder.toString()));
});
}
return undefined;
});
};
const FIND_IN_FOLDER_ID = 'filesExplorer.findInFolder';
KeybindingsRegistry.registerCommandAndKeybindingRule({
id: FIND_IN_FOLDER_ID,
weight: KeybindingWeight.WorkbenchContrib,
when: ContextKeyExpr.and(FilesExplorerFocusCondition, ExplorerFolderContext, ResourceContextKey.Scheme.isEqualTo(Schemas.file)), // todo@remote
primary: KeyMod.Shift | KeyMod.Alt | KeyCode.KEY_F,
handler: searchInFolderCommand
});
CommandsRegistry.registerCommand({
id: ClearSearchResultsAction.ID,
handler: (accessor, args: any) => {
accessor.get(IInstantiationService).createInstance(ClearSearchResultsAction, ClearSearchResultsAction.ID, '').run();
}
});
CommandsRegistry.registerCommand({
id: RefreshAction.ID,
handler: (accessor, args: any) => {
accessor.get(IInstantiationService).createInstance(RefreshAction, RefreshAction.ID, '').run();
}
});
const FIND_IN_WORKSPACE_ID = 'filesExplorer.findInWorkspace';
CommandsRegistry.registerCommand({
id: FIND_IN_WORKSPACE_ID,
handler: (accessor) => {
return openSearchView(accessor.get(IViewletService), accessor.get(IPanelService), accessor.get(IConfigurationService), true).then(searchView => {
searchView.searchInFolders(null);
});
}
});
MenuRegistry.appendMenuItem(MenuId.ExplorerContext, {
group: '4_search',
order: 10,
command: {
id: FIND_IN_FOLDER_ID,
title: nls.localize('findInFolder', "Find in Folder...")
},
when: ContextKeyExpr.and(ExplorerFolderContext, ResourceContextKey.Scheme.isEqualTo(Schemas.file)) // todo@remote
});
MenuRegistry.appendMenuItem(MenuId.ExplorerContext, {
group: '4_search',
order: 10,
command: {
id: FIND_IN_WORKSPACE_ID,
title: nls.localize('findInWorkspace', "Find in Workspace...")
},
when: ContextKeyExpr.and(ExplorerRootContext, ExplorerFolderContext.toNegated())
});
class ShowAllSymbolsAction extends Action {
static readonly ID = 'workbench.action.showAllSymbols';
static readonly LABEL = nls.localize('showTriggerActions', "Go to Symbol in Workspace...");
static readonly ALL_SYMBOLS_PREFIX = '#';
constructor(
actionId: string, actionLabel: string,
@IQuickOpenService private readonly quickOpenService: IQuickOpenService,
@ICodeEditorService private readonly editorService: ICodeEditorService) {
super(actionId, actionLabel);
this.enabled = !!this.quickOpenService;
}
run(context?: any): Promise<void> {
let prefix = ShowAllSymbolsAction.ALL_SYMBOLS_PREFIX;
let inputSelection: { start: number; end: number; } = undefined;
const editor = this.editorService.getFocusedCodeEditor();
const word = editor && getSelectionSearchString(editor);
if (word) {
prefix = prefix + word;
inputSelection = { start: 1, end: word.length + 1 };
}
this.quickOpenService.show(prefix, { inputSelection });
return Promise.resolve(undefined);
}
}
Registry.as<ViewletRegistry>(ViewletExtensions.Viewlets).registerViewlet(new ViewletDescriptor(
SearchViewlet,
VIEWLET_ID,
nls.localize('name', "Search"),
'search',
1
));
class RegisterSearchViewContribution implements IWorkbenchContribution {
constructor(
@IViewletService viewletService: IViewletService,
@IPanelService panelService: IPanelService,
@IConfigurationService configurationService: IConfigurationService
) {
const viewsRegistry = Registry.as<IViewsRegistry>(ViewExtensions.ViewsRegistry);
const updateSearchViewLocation = (open: boolean) => {
const config = configurationService.getValue<ISearchConfiguration>();
if (config.search.location === 'panel') {
viewsRegistry.deregisterViews(viewsRegistry.getViews(VIEW_CONTAINER), VIEW_CONTAINER);
Registry.as<PanelRegistry>(PanelExtensions.Panels).registerPanel(new PanelDescriptor(
SearchPanel,
PANEL_ID,
nls.localize('name', "Search"),
'search',
10
));
if (open) {
panelService.openPanel(PANEL_ID);
}
} else {
Registry.as<PanelRegistry>(PanelExtensions.Panels).deregisterPanel(PANEL_ID);
viewsRegistry.registerViews([{ id: VIEW_ID, name: nls.localize('search', "Search"), ctorDescriptor: { ctor: SearchView }, canToggleVisibility: false }], VIEW_CONTAINER);
if (open) {
viewletService.openViewlet(VIEWLET_ID);
}
}
};
configurationService.onDidChangeConfiguration(e => {
if (e.affectsConfiguration('search.location')) {
updateSearchViewLocation(true);
}
});
updateSearchViewLocation(false);
}
}
Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench).registerWorkbenchContribution(RegisterSearchViewContribution, LifecyclePhase.Starting);
// Actions
const registry = Registry.as<IWorkbenchActionRegistry>(ActionExtensions.WorkbenchActions);
// Show Search and Find in Files are redundant, but we can't break keybindings by removing one. So it's the same action, same keybinding, registered to different IDs.
// Show Search 'when' is redundant but if the two conflict with exactly the same keybinding and 'when' clause, then they can show up as "unbound" - #51780
registry.registerWorkbenchAction(new SyncActionDescriptor(OpenSearchViewletAction, VIEWLET_ID, OpenSearchViewletAction.LABEL, { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_F }, Constants.SearchViewVisibleKey.toNegated()), 'View: Show Search', nls.localize('view', "View"));
registry.registerWorkbenchAction(new SyncActionDescriptor(FindInFilesAction, Constants.FindInFilesActionId, nls.localize('findInFiles', "Find in Files"), { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_F }), 'Find in Files', category);
MenuRegistry.appendMenuItem(MenuId.MenubarEditMenu, {
group: '4_find_global',
command: {
id: Constants.FindInFilesActionId,
title: nls.localize({ key: 'miFindInFiles', comment: ['&& denotes a mnemonic'] }, "Find &&in Files")
},
order: 1
});
registry.registerWorkbenchAction(new SyncActionDescriptor(FocusNextSearchResultAction, FocusNextSearchResultAction.ID, FocusNextSearchResultAction.LABEL, { primary: KeyCode.F4 }, ContextKeyExpr.and(Constants.HasSearchResults)), 'Focus Next Search Result', category);
registry.registerWorkbenchAction(new SyncActionDescriptor(FocusPreviousSearchResultAction, FocusPreviousSearchResultAction.ID, FocusPreviousSearchResultAction.LABEL, { primary: KeyMod.Shift | KeyCode.F4 }, ContextKeyExpr.and(Constants.HasSearchResults)), 'Focus Previous Search Result', category);
registry.registerWorkbenchAction(new SyncActionDescriptor(ReplaceInFilesAction, ReplaceInFilesAction.ID, ReplaceInFilesAction.LABEL, { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_H }), 'Replace in Files', category);
MenuRegistry.appendMenuItem(MenuId.MenubarEditMenu, {
group: '4_find_global',
command: {
id: ReplaceInFilesAction.ID,
title: nls.localize({ key: 'miReplaceInFiles', comment: ['&& denotes a mnemonic'] }, "Replace &&in Files")
},
order: 2
});
KeybindingsRegistry.registerCommandAndKeybindingRule(objects.assign({
id: Constants.ToggleCaseSensitiveCommandId,
weight: KeybindingWeight.WorkbenchContrib,
when: ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.SearchViewFocusedKey, Constants.FileMatchOrFolderMatchFocusKey.toNegated()),
handler: toggleCaseSensitiveCommand
}, ToggleCaseSensitiveKeybinding));
KeybindingsRegistry.registerCommandAndKeybindingRule(objects.assign({
id: Constants.ToggleWholeWordCommandId,
weight: KeybindingWeight.WorkbenchContrib,
when: ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.SearchViewFocusedKey),
handler: toggleWholeWordCommand
}, ToggleWholeWordKeybinding));
KeybindingsRegistry.registerCommandAndKeybindingRule(objects.assign({
id: Constants.ToggleRegexCommandId,
weight: KeybindingWeight.WorkbenchContrib,
when: ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.SearchViewFocusedKey),
handler: toggleRegexCommand
}, ToggleRegexKeybinding));
registry.registerWorkbenchAction(new SyncActionDescriptor(CollapseDeepestExpandedLevelAction, CollapseDeepestExpandedLevelAction.ID, CollapseDeepestExpandedLevelAction.LABEL), 'Search: Collapse All', category);
registry.registerWorkbenchAction(new SyncActionDescriptor(ShowAllSymbolsAction, ShowAllSymbolsAction.ID, ShowAllSymbolsAction.LABEL, { primary: KeyMod.CtrlCmd | KeyCode.KEY_T }), 'Go to Symbol in Workspace...');
registry.registerWorkbenchAction(new SyncActionDescriptor(RefreshAction, RefreshAction.ID, RefreshAction.LABEL), 'Search: Refresh', category);
registry.registerWorkbenchAction(new SyncActionDescriptor(ClearSearchResultsAction, ClearSearchResultsAction.ID, ClearSearchResultsAction.LABEL), 'Search: Clear', category);
// Register Quick Open Handler
Registry.as<IQuickOpenRegistry>(QuickOpenExtensions.Quickopen).registerDefaultQuickOpenHandler(
new QuickOpenHandlerDescriptor(
OpenAnythingHandler,
OpenAnythingHandler.ID,
'',
defaultQuickOpenContextKey,
nls.localize('openAnythingHandlerDescription', "Go to File")
)
);
Registry.as<IQuickOpenRegistry>(QuickOpenExtensions.Quickopen).registerQuickOpenHandler(
new QuickOpenHandlerDescriptor(
OpenSymbolHandler,
OpenSymbolHandler.ID,
ShowAllSymbolsAction.ALL_SYMBOLS_PREFIX,
'inWorkspaceSymbolsPicker',
[
{
prefix: ShowAllSymbolsAction.ALL_SYMBOLS_PREFIX,
needsEditor: false,
description: nls.localize('openSymbolDescriptionNormal', "Go to Symbol in Workspace")
}
]
)
);
// Configuration
const configurationRegistry = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration);
configurationRegistry.registerConfiguration({
id: 'search',
order: 13,
title: nls.localize('searchConfigurationTitle', "Search"),
type: 'object',
properties: {
'search.exclude': {
type: 'object',
markdownDescription: nls.localize('exclude', "Configure glob patterns for excluding files and folders in searches. Inherits all glob patterns from the `#files.exclude#` setting. Read more about glob patterns [here](https://code.visualstudio.com/docs/editor/codebasics#_advanced-search-options)."),
default: { '**/node_modules': true, '**/bower_components': true },
additionalProperties: {
anyOf: [
{
type: 'boolean',
description: nls.localize('exclude.boolean', "The glob pattern to match file paths against. Set to true or false to enable or disable the pattern."),
},
{
type: 'object',
properties: {
when: {
type: 'string', // expression ({ "**/*.js": { "when": "$(basename).js" } })
pattern: '\\w*\\$\\(basename\\)\\w*',
default: '$(basename).ext',
description: nls.localize('exclude.when', 'Additional check on the siblings of a matching file. Use $(basename) as variable for the matching file name.')
}
}
}
]
},
scope: ConfigurationScope.RESOURCE
},
'search.useRipgrep': {
type: 'boolean',
description: nls.localize('useRipgrep', "This setting is deprecated and now falls back on \"search.usePCRE2\"."),
deprecationMessage: nls.localize('useRipgrepDeprecated', "Deprecated. Consider \"search.usePCRE2\" for advanced regex feature support."),
default: true
},
'search.maintainFileSearchCache': {
type: 'boolean',
description: nls.localize('search.maintainFileSearchCache', "When enabled, the searchService process will be kept alive instead of being shut down after an hour of inactivity. This will keep the file search cache in memory."),
default: false
},
'search.useIgnoreFiles': {
type: 'boolean',
markdownDescription: nls.localize('useIgnoreFiles', "Controls whether to use `.gitignore` and `.ignore` files when searching for files."),
default: true,
scope: ConfigurationScope.RESOURCE
},
'search.useGlobalIgnoreFiles': {
type: 'boolean',
markdownDescription: nls.localize('useGlobalIgnoreFiles', "Controls whether to use global `.gitignore` and `.ignore` files when searching for files."),
default: false,
scope: ConfigurationScope.RESOURCE
},
'search.quickOpen.includeSymbols': {
type: 'boolean',
description: nls.localize('search.quickOpen.includeSymbols', "Whether to include results from a global symbol search in the file results for Quick Open."),
default: false
},
'search.quickOpen.includeHistory': {
type: 'boolean',
description: nls.localize('search.quickOpen.includeHistory', "Whether to include results from recently opened files in the file results for Quick Open."),
default: true
},
'search.followSymlinks': {
type: 'boolean',
description: nls.localize('search.followSymlinks', "Controls whether to follow symlinks while searching."),
default: true
},
'search.smartCase': {
type: 'boolean',
description: nls.localize('search.smartCase', "Search case-insensitively if the pattern is all lowercase, otherwise, search case-sensitively."),
default: false
},
'search.globalFindClipboard': {
type: 'boolean',
default: false,
description: nls.localize('search.globalFindClipboard', "Controls whether the search view should read or modify the shared find clipboard on macOS."),
included: platform.isMacintosh
},
'search.location': {
type: 'string',
enum: ['sidebar', 'panel'],
default: 'sidebar',
description: nls.localize('search.location', "Controls whether the search will be shown as a view in the sidebar or as a panel in the panel area for more horizontal space."),
},
'search.collapseResults': {
type: 'string',
enum: ['auto', 'alwaysCollapse', 'alwaysExpand'],
enumDescriptions: [
'Files with less than 10 results are expanded. Others are collapsed.',
'',
''
],
default: 'auto',
description: nls.localize('search.collapseAllResults', "Controls whether the search results will be collapsed or expanded."),
},
'search.useReplacePreview': {
type: 'boolean',
default: true,
description: nls.localize('search.useReplacePreview', "Controls whether to open Replace Preview when selecting or replacing a match."),
},
'search.showLineNumbers': {
type: 'boolean',
default: false,
description: nls.localize('search.showLineNumbers', "Controls whether to show line numbers for search results."),
},
'searchRipgrep.enable': {
type: 'boolean',
default: false,
deprecationMessage: nls.localize('search.searchRipgrepEnableDeprecated', "Deprecated. Use \"search.runInExtensionHost\" instead"),
description: nls.localize('search.searchRipgrepEnable', "Whether to run search in the extension host")
},
'search.runInExtensionHost': {
type: 'boolean',
default: false,
description: nls.localize('search.runInExtensionHost', "Whether to run search in the extension host. Requires a restart to take effect.")
},
'search.usePCRE2': {
type: 'boolean',
default: false,
description: nls.localize('search.usePCRE2', "Whether to use the PCRE2 regex engine in text search. This enables using some advanced regex features like lookahead and backreferences. However, not all PCRE2 features are supported - only features that are also supported by JavaScript.")
},
'search.actionsPosition': {
type: 'string',
enum: ['auto', 'right'],
enumDescriptions: [
nls.localize('search.actionsPositionAuto', "Position the actionbar to the right when the search view is narrow, and immediately after the content when the search view is wide."),
nls.localize('search.actionsPositionRight', "Always position the actionbar to the right."),
],
default: 'auto',
description: nls.localize('search.actionsPosition', "Controls the positioning of the actionbar on rows in the search view.")
}
}
});
registerLanguageCommand('_executeWorkspaceSymbolProvider', function (accessor, args: { query: string; }) {
const { query } = args;
if (typeof query !== 'string') {
throw illegalArgument();
}
return getWorkspaceSymbols(query);
});
// View menu
MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, {
group: '3_views',
command: {
id: VIEWLET_ID,
title: nls.localize({ key: 'miViewSearch', comment: ['&& denotes a mnemonic'] }, "&&Search")
},
order: 2
});
// Go to menu
// {{SQL CARBON EDIT}} - Disable unused menu item
// MenuRegistry.appendMenuItem(MenuId.MenubarGoMenu, {
// group: '3_global_nav',
// command: {
// id: 'workbench.action.showAllSymbols',
// title: nls.localize({ key: 'miGotoSymbolInWorkspace', comment: ['&& denotes a mnemonic'] }, "Go to Symbol in &&Workspace...")
// },
// order: 2
// });
// {{SQL CARBON EDIT}} - End

View File

@@ -0,0 +1,776 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as DOM from 'vs/base/browser/dom';
import { Action } from 'vs/base/common/actions';
import { INavigator } from 'vs/base/common/iterator';
import { createKeybinding, ResolvedKeybinding } from 'vs/base/common/keyCodes';
import { normalizeDriveLetter } from 'vs/base/common/labels';
import { Schemas } from 'vs/base/common/network';
import { normalize } from 'vs/base/common/path';
import { isWindows, OS } from 'vs/base/common/platform';
import { repeat } from 'vs/base/common/strings';
import { URI } from 'vs/base/common/uri';
import * as nls from 'vs/nls';
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
import { ICommandHandler } from 'vs/platform/commands/common/commands';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { getSelectionKeyboardEvent, WorkbenchObjectTree } from 'vs/platform/list/browser/listService';
import { SearchView } from 'vs/workbench/contrib/search/browser/searchView';
import * as Constants from 'vs/workbench/contrib/search/common/constants';
import { IReplaceService } from 'vs/workbench/contrib/search/common/replace';
import { BaseFolderMatch, FileMatch, FileMatchOrMatch, FolderMatch, Match, RenderableMatch, searchMatchComparer, SearchResult } from 'vs/workbench/contrib/search/common/searchModel';
import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { IPanelService } from 'vs/workbench/services/panel/common/panelService';
import { ISearchConfiguration, VIEWLET_ID, PANEL_ID } from 'vs/workbench/services/search/common/search';
import { ISearchHistoryService } from 'vs/workbench/contrib/search/common/searchHistoryService';
import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet';
import { SearchViewlet } from 'vs/workbench/contrib/search/browser/searchViewlet';
import { SearchPanel } from 'vs/workbench/contrib/search/browser/searchPanel';
export function isSearchViewFocused(viewletService: IViewletService, panelService: IPanelService): boolean {
const searchView = getSearchView(viewletService, panelService);
const activeElement = document.activeElement;
return !!(searchView && activeElement && DOM.isAncestor(activeElement, searchView.getContainer()));
}
export function appendKeyBindingLabel(label: string, inputKeyBinding: number | ResolvedKeybinding | undefined, keyBindingService2: IKeybindingService): string {
if (typeof inputKeyBinding === 'number') {
const keybinding = createKeybinding(inputKeyBinding, OS);
if (keybinding) {
const resolvedKeybindings = keyBindingService2.resolveKeybinding(keybinding);
return doAppendKeyBindingLabel(label, resolvedKeybindings.length > 0 ? resolvedKeybindings[0] : undefined);
}
return doAppendKeyBindingLabel(label, undefined);
} else {
return doAppendKeyBindingLabel(label, inputKeyBinding);
}
}
export function openSearchView(viewletService: IViewletService, panelService: IPanelService, configurationService: IConfigurationService, focus?: boolean): Promise<SearchView> {
if (configurationService.getValue<ISearchConfiguration>().search.location === 'panel') {
return Promise.resolve((panelService.openPanel(PANEL_ID, focus) as SearchPanel).getSearchView());
}
return viewletService.openViewlet(VIEWLET_ID, focus).then(viewlet => (viewlet as SearchViewlet).getSearchView());
}
export function getSearchView(viewletService: IViewletService, panelService: IPanelService): SearchView | null {
const activeViewlet = viewletService.getActiveViewlet();
if (activeViewlet && activeViewlet.getId() === VIEWLET_ID) {
return (activeViewlet as SearchViewlet).getSearchView();
}
const activePanel = panelService.getActivePanel();
if (activePanel && activePanel.getId() === PANEL_ID) {
return (activePanel as SearchPanel).getSearchView();
}
return null;
}
function doAppendKeyBindingLabel(label: string, keyBinding: ResolvedKeybinding | undefined): string {
return keyBinding ? label + ' (' + keyBinding.getLabel() + ')' : label;
}
export const toggleCaseSensitiveCommand = (accessor: ServicesAccessor) => {
const searchView = getSearchView(accessor.get(IViewletService), accessor.get(IPanelService));
if (searchView) {
searchView.toggleCaseSensitive();
}
};
export const toggleWholeWordCommand = (accessor: ServicesAccessor) => {
const searchView = getSearchView(accessor.get(IViewletService), accessor.get(IPanelService));
if (searchView) {
searchView.toggleWholeWords();
}
};
export const toggleRegexCommand = (accessor: ServicesAccessor) => {
const searchView = getSearchView(accessor.get(IViewletService), accessor.get(IPanelService));
if (searchView) {
searchView.toggleRegex();
}
};
export class FocusNextInputAction extends Action {
static readonly ID = 'search.focus.nextInputBox';
constructor(id: string, label: string,
@IViewletService private readonly viewletService: IViewletService,
@IPanelService private readonly panelService: IPanelService
) {
super(id, label);
}
run(): Promise<any> {
const searchView = getSearchView(this.viewletService, this.panelService);
if (searchView) {
searchView.focusNextInputBox();
}
return Promise.resolve(null);
}
}
export class FocusPreviousInputAction extends Action {
static readonly ID = 'search.focus.previousInputBox';
constructor(id: string, label: string,
@IViewletService private readonly viewletService: IViewletService,
@IPanelService private readonly panelService: IPanelService
) {
super(id, label);
}
run(): Promise<any> {
const searchView = getSearchView(this.viewletService, this.panelService);
if (searchView) {
searchView.focusPreviousInputBox();
}
return Promise.resolve(null);
}
}
export abstract class FindOrReplaceInFilesAction extends Action {
constructor(id: string, label: string, protected viewletService: IViewletService, protected panelService: IPanelService, protected configurationService: IConfigurationService,
private expandSearchReplaceWidget: boolean
) {
super(id, label);
}
run(): Promise<any> {
return openSearchView(this.viewletService, this.panelService, this.configurationService, false).then(openedView => {
const searchAndReplaceWidget = openedView.searchAndReplaceWidget;
searchAndReplaceWidget.toggleReplace(this.expandSearchReplaceWidget);
const updatedText = openedView.updateTextFromSelection(!this.expandSearchReplaceWidget);
openedView.searchAndReplaceWidget.focus(undefined, updatedText, updatedText);
});
}
}
export class FindInFilesAction extends FindOrReplaceInFilesAction {
static readonly LABEL = nls.localize('findInFiles', "Find in Files");
constructor(id: string, label: string,
@IViewletService viewletService: IViewletService,
@IPanelService panelService: IPanelService,
@IConfigurationService configurationService: IConfigurationService
) {
super(id, label, viewletService, panelService, configurationService, /*expandSearchReplaceWidget=*/false);
}
}
export class OpenSearchViewletAction extends FindOrReplaceInFilesAction {
static readonly LABEL = nls.localize('showSearch', "Show Search");
constructor(id: string, label: string,
@IViewletService viewletService: IViewletService,
@IPanelService panelService: IPanelService,
@IEditorGroupsService private readonly editorGroupService: IEditorGroupsService,
@IConfigurationService configurationService: IConfigurationService
) {
super(id, label, viewletService, panelService, configurationService, /*expandSearchReplaceWidget=*/false);
}
run(): Promise<any> {
// Pass focus to viewlet if not open or focused
if (this.otherViewletShowing() || !isSearchViewFocused(this.viewletService, this.panelService)) {
return super.run();
}
// Otherwise pass focus to editor group
this.editorGroupService.activeGroup.focus();
return Promise.resolve(true);
}
private otherViewletShowing(): boolean {
return !getSearchView(this.viewletService, this.panelService);
}
}
export class ReplaceInFilesAction extends FindOrReplaceInFilesAction {
static readonly ID = 'workbench.action.replaceInFiles';
static readonly LABEL = nls.localize('replaceInFiles', "Replace in Files");
constructor(id: string, label: string,
@IViewletService viewletService: IViewletService,
@IPanelService panelService: IPanelService,
@IConfigurationService configurationService: IConfigurationService
) {
super(id, label, viewletService, panelService, configurationService, /*expandSearchReplaceWidget=*/true);
}
}
export class CloseReplaceAction extends Action {
constructor(id: string, label: string,
@IViewletService private readonly viewletService: IViewletService,
@IPanelService private readonly panelService: IPanelService
) {
super(id, label);
}
run(): Promise<any> {
const searchView = getSearchView(this.viewletService, this.panelService);
if (searchView) {
searchView.searchAndReplaceWidget.toggleReplace(false);
searchView.searchAndReplaceWidget.focus();
}
return Promise.resolve(null);
}
}
export class RefreshAction extends Action {
static readonly ID: string = 'search.action.refreshSearchResults';
static LABEL: string = nls.localize('RefreshAction.label', "Refresh");
private searchView: SearchView | null;
constructor(id: string, label: string,
@IViewletService private readonly viewletService: IViewletService,
@IPanelService private readonly panelService: IPanelService
) {
super(id, label, 'search-action refresh');
this.searchView = getSearchView(this.viewletService, this.panelService);
}
get enabled(): boolean {
return !!this.searchView && this.searchView.isSearchSubmitted();
}
update(): void {
this._setEnabled(this.enabled);
}
run(): Promise<void> {
const searchView = getSearchView(this.viewletService, this.panelService);
if (searchView) {
searchView.onQueryChanged();
}
return Promise.resolve();
}
}
export class CollapseDeepestExpandedLevelAction extends Action {
static readonly ID: string = 'search.action.collapseSearchResults';
static LABEL: string = nls.localize('CollapseDeepestExpandedLevelAction.label', "Collapse All");
constructor(id: string, label: string,
@IViewletService private readonly viewletService: IViewletService,
@IPanelService private readonly panelService: IPanelService
) {
super(id, label, 'search-action collapse');
this.update();
}
update(): void {
const searchView = getSearchView(this.viewletService, this.panelService);
this.enabled = !!searchView && searchView.hasSearchResults();
}
run(): Promise<void> {
const searchView = getSearchView(this.viewletService, this.panelService);
if (searchView) {
const viewer = searchView.getControl();
/**
* one level to collapse so collapse everything. If FolderMatch, check if there are visible grandchildren,
* i.e. if Matches are returned by the navigator, and if so, collapse to them, otherwise collapse all levels.
*/
const navigator = viewer.navigate();
let node = navigator.first();
let collapseFileMatchLevel = false;
if (node instanceof BaseFolderMatch) {
while (node = navigator.next()) {
if (node instanceof Match) {
collapseFileMatchLevel = true;
break;
}
}
}
if (collapseFileMatchLevel) {
node = navigator.first();
do {
if (node instanceof FileMatch) {
viewer.collapse(node);
}
} while (node = navigator.next());
} else {
viewer.collapseAll();
}
viewer.domFocus();
viewer.focusFirst();
}
return Promise.resolve(undefined);
}
}
export class ClearSearchResultsAction extends Action {
static readonly ID: string = 'search.action.clearSearchResults';
static LABEL: string = nls.localize('ClearSearchResultsAction.label', "Clear Search Results");
constructor(id: string, label: string,
@IViewletService private readonly viewletService: IViewletService,
@IPanelService private readonly panelService: IPanelService
) {
super(id, label, 'search-action clear-search-results');
this.update();
}
update(): void {
const searchView = getSearchView(this.viewletService, this.panelService);
this.enabled = !!searchView && (!searchView.allSearchFieldsClear() || searchView.hasSearchResults());
}
run(): Promise<void> {
const searchView = getSearchView(this.viewletService, this.panelService);
if (searchView) {
searchView.clearSearchResults();
}
return Promise.resolve();
}
}
export class CancelSearchAction extends Action {
static readonly ID: string = 'search.action.cancelSearch';
static LABEL: string = nls.localize('CancelSearchAction.label', "Cancel Search");
constructor(id: string, label: string,
@IViewletService private readonly viewletService: IViewletService,
@IPanelService private readonly panelService: IPanelService
) {
super(id, label, 'search-action cancel-search');
this.update();
}
update(): void {
const searchView = getSearchView(this.viewletService, this.panelService);
this.enabled = !!searchView && searchView.isSearching();
}
run(): Promise<void> {
const searchView = getSearchView(this.viewletService, this.panelService);
if (searchView) {
searchView.cancelSearch();
}
return Promise.resolve(undefined);
}
}
export class FocusNextSearchResultAction extends Action {
static readonly ID = 'search.action.focusNextSearchResult';
static readonly LABEL = nls.localize('FocusNextSearchResult.label', "Focus Next Search Result");
constructor(id: string, label: string,
@IViewletService private readonly viewletService: IViewletService,
@IPanelService private readonly panelService: IPanelService,
@IConfigurationService private readonly configurationService: IConfigurationService
) {
super(id, label);
}
run(): Promise<any> {
return openSearchView(this.viewletService, this.panelService, this.configurationService).then(searchView => {
searchView.selectNextMatch();
});
}
}
export class FocusPreviousSearchResultAction extends Action {
static readonly ID = 'search.action.focusPreviousSearchResult';
static readonly LABEL = nls.localize('FocusPreviousSearchResult.label', "Focus Previous Search Result");
constructor(id: string, label: string,
@IViewletService private readonly viewletService: IViewletService,
@IPanelService private readonly panelService: IPanelService,
@IConfigurationService private readonly configurationService: IConfigurationService
) {
super(id, label);
}
run(): Promise<any> {
return openSearchView(this.viewletService, this.panelService, this.configurationService).then(searchView => {
searchView.selectPreviousMatch();
});
}
}
export abstract class AbstractSearchAndReplaceAction extends Action {
/**
* Returns element to focus after removing the given element
*/
getElementToFocusAfterRemoved(viewer: WorkbenchObjectTree<RenderableMatch>, elementToBeRemoved: RenderableMatch): RenderableMatch {
const elementToFocus = this.getNextElementAfterRemoved(viewer, elementToBeRemoved);
return elementToFocus || this.getPreviousElementAfterRemoved(viewer, elementToBeRemoved);
}
getNextElementAfterRemoved(viewer: WorkbenchObjectTree<RenderableMatch>, element: RenderableMatch): RenderableMatch {
const navigator: INavigator<any> = viewer.navigate(element);
if (element instanceof BaseFolderMatch) {
while (!!navigator.next() && !(navigator.current() instanceof BaseFolderMatch)) { }
} else if (element instanceof FileMatch) {
while (!!navigator.next() && !(navigator.current() instanceof FileMatch)) { }
} else {
while (navigator.next() && !(navigator.current() instanceof Match)) {
viewer.expand(navigator.current());
}
}
return navigator.current();
}
getPreviousElementAfterRemoved(viewer: WorkbenchObjectTree<RenderableMatch>, element: RenderableMatch): RenderableMatch {
const navigator: INavigator<any> = viewer.navigate(element);
let previousElement = navigator.previous();
// Hence take the previous element.
const parent = element.parent();
if (parent === previousElement) {
previousElement = navigator.previous();
}
if (parent instanceof FileMatch && parent.parent() === previousElement) {
previousElement = navigator.previous();
}
// If the previous element is a File or Folder, expand it and go to its last child.
// Spell out the two cases, would be too easy to create an infinite loop, like by adding another level...
if (element instanceof Match && previousElement && previousElement instanceof BaseFolderMatch) {
navigator.next();
viewer.expand(previousElement);
previousElement = navigator.previous();
}
if (element instanceof Match && previousElement && previousElement instanceof FileMatch) {
navigator.next();
viewer.expand(previousElement);
previousElement = navigator.previous();
}
return previousElement;
}
}
export class RemoveAction extends AbstractSearchAndReplaceAction {
static LABEL = nls.localize('RemoveAction.label', "Dismiss");
constructor(
private viewer: WorkbenchObjectTree<RenderableMatch>,
private element: RenderableMatch
) {
super('remove', RemoveAction.LABEL, 'action-remove');
}
run(): Promise<any> {
const currentFocusElement = this.viewer.getFocus()[0];
const nextFocusElement = !currentFocusElement || currentFocusElement instanceof SearchResult || elementIsEqualOrParent(currentFocusElement, this.element) ?
this.getElementToFocusAfterRemoved(this.viewer, this.element) :
null;
if (nextFocusElement) {
this.viewer.reveal(nextFocusElement);
this.viewer.setFocus([nextFocusElement], getSelectionKeyboardEvent());
}
this.element.parent().remove(<any>this.element);
this.viewer.domFocus();
return Promise.resolve();
}
}
function elementIsEqualOrParent(element: RenderableMatch, testParent: RenderableMatch | SearchResult): boolean {
do {
if (element === testParent) {
return true;
}
} while (!(element.parent() instanceof SearchResult) && (element = <RenderableMatch>element.parent()));
return false;
}
export class ReplaceAllAction extends AbstractSearchAndReplaceAction {
static readonly LABEL = nls.localize('file.replaceAll.label', "Replace All");
constructor(
private viewlet: SearchView,
private fileMatch: FileMatch,
@IKeybindingService keyBindingService: IKeybindingService
) {
super(Constants.ReplaceAllInFileActionId, appendKeyBindingLabel(ReplaceAllAction.LABEL, keyBindingService.lookupKeybinding(Constants.ReplaceAllInFileActionId), keyBindingService), 'action-replace-all');
}
run(): Promise<any> {
const tree = this.viewlet.getControl();
const nextFocusElement = this.getElementToFocusAfterRemoved(tree, this.fileMatch);
return this.fileMatch.parent().replace(this.fileMatch).then(() => {
if (nextFocusElement) {
tree.setFocus([nextFocusElement], getSelectionKeyboardEvent());
}
tree.domFocus();
this.viewlet.open(this.fileMatch, true);
});
}
}
export class ReplaceAllInFolderAction extends AbstractSearchAndReplaceAction {
static readonly LABEL = nls.localize('file.replaceAll.label', "Replace All");
constructor(private viewer: WorkbenchObjectTree<RenderableMatch>, private folderMatch: FolderMatch,
@IKeybindingService keyBindingService: IKeybindingService
) {
super(Constants.ReplaceAllInFolderActionId, appendKeyBindingLabel(ReplaceAllInFolderAction.LABEL, keyBindingService.lookupKeybinding(Constants.ReplaceAllInFolderActionId), keyBindingService), 'action-replace-all');
}
run(): Promise<any> {
const nextFocusElement = this.getElementToFocusAfterRemoved(this.viewer, this.folderMatch);
return this.folderMatch.replaceAll().then(() => {
if (nextFocusElement) {
this.viewer.setFocus([nextFocusElement], getSelectionKeyboardEvent());
}
this.viewer.domFocus();
});
}
}
export class ReplaceAction extends AbstractSearchAndReplaceAction {
static readonly LABEL = nls.localize('match.replace.label', "Replace");
constructor(private viewer: WorkbenchObjectTree<RenderableMatch>, private element: Match, private viewlet: SearchView,
@IReplaceService private readonly replaceService: IReplaceService,
@IKeybindingService keyBindingService: IKeybindingService,
@IEditorService private readonly editorService: IEditorService,
@IConfigurationService private readonly configurationService: IConfigurationService) {
super(Constants.ReplaceActionId, appendKeyBindingLabel(ReplaceAction.LABEL, keyBindingService.lookupKeybinding(Constants.ReplaceActionId), keyBindingService), 'action-replace');
}
run(): Promise<any> {
this.enabled = false;
return this.element.parent().replace(this.element).then(() => {
const elementToFocus = this.getElementToFocusAfterReplace();
if (elementToFocus) {
this.viewer.setFocus([elementToFocus], getSelectionKeyboardEvent());
}
return this.getElementToShowReplacePreview(elementToFocus);
}).then(elementToShowReplacePreview => {
this.viewer.domFocus();
const useReplacePreview = this.configurationService.getValue<ISearchConfiguration>().search.useReplacePreview;
if (!useReplacePreview || !elementToShowReplacePreview || this.hasToOpenFile()) {
this.viewlet.open(this.element, true);
} else {
this.replaceService.openReplacePreview(elementToShowReplacePreview, true);
}
});
}
private getElementToFocusAfterReplace(): Match {
const navigator: INavigator<any> = this.viewer.navigate();
let fileMatched = false;
let elementToFocus: any = null;
do {
elementToFocus = navigator.current();
if (elementToFocus instanceof Match) {
if (elementToFocus.parent().id() === this.element.parent().id()) {
fileMatched = true;
if (this.element.range().getStartPosition().isBeforeOrEqual((<Match>elementToFocus).range().getStartPosition())) {
// Closest next match in the same file
break;
}
} else if (fileMatched) {
// First match in the next file (if expanded)
break;
}
} else if (fileMatched) {
if (this.viewer.isCollapsed(elementToFocus)) {
// Next file match (if collapsed)
break;
}
}
} while (!!navigator.next());
return elementToFocus;
}
private async getElementToShowReplacePreview(elementToFocus: FileMatchOrMatch): Promise<Match | null> {
if (this.hasSameParent(elementToFocus)) {
return <Match>elementToFocus;
}
const previousElement = await this.getPreviousElementAfterRemoved(this.viewer, this.element);
if (this.hasSameParent(previousElement)) {
return <Match>previousElement;
}
return null;
}
private hasSameParent(element: RenderableMatch): boolean {
return element && element instanceof Match && element.parent().resource() === this.element.parent().resource();
}
private hasToOpenFile(): boolean {
const activeEditor = this.editorService.activeEditor;
const file = activeEditor ? activeEditor.getResource() : undefined;
if (file) {
return file.toString() === this.element.parent().resource().toString();
}
return false;
}
}
function uriToClipboardString(resource: URI): string {
return resource.scheme === Schemas.file ? normalize(normalizeDriveLetter(resource.fsPath)) : resource.toString();
}
export const copyPathCommand: ICommandHandler = (accessor, fileMatch: FileMatch | FolderMatch) => {
const clipboardService = accessor.get(IClipboardService);
const text = uriToClipboardString(fileMatch.resource());
clipboardService.writeText(text);
};
function matchToString(match: Match, indent = 0): string {
const getFirstLinePrefix = () => `${match.range().startLineNumber},${match.range().startColumn}`;
const getOtherLinePrefix = (i: number) => match.range().startLineNumber + i + '';
const fullMatchLines = match.fullPreviewLines();
const largestPrefixSize = fullMatchLines.reduce((largest, _, i) => {
const thisSize = i === 0 ?
getFirstLinePrefix().length :
getOtherLinePrefix(i).length;
return Math.max(thisSize, largest);
}, 0);
const formattedLines = fullMatchLines
.map((line, i) => {
const prefix = i === 0 ?
getFirstLinePrefix() :
getOtherLinePrefix(i);
const paddingStr = repeat(' ', largestPrefixSize - prefix.length);
const indentStr = repeat(' ', indent);
return `${indentStr}${prefix}: ${paddingStr}${line}`;
});
return formattedLines.join('\n');
}
const lineDelimiter = isWindows ? '\r\n' : '\n';
function fileMatchToString(fileMatch: FileMatch, maxMatches: number): { text: string, count: number } {
const matchTextRows = fileMatch.matches()
.sort(searchMatchComparer)
.slice(0, maxMatches)
.map(match => matchToString(match, 2));
return {
text: `${uriToClipboardString(fileMatch.resource())}${lineDelimiter}${matchTextRows.join(lineDelimiter)}`,
count: matchTextRows.length
};
}
function folderMatchToString(folderMatch: FolderMatch | BaseFolderMatch, maxMatches: number): { text: string, count: number } {
const fileResults: string[] = [];
let numMatches = 0;
const matches = folderMatch.matches().sort(searchMatchComparer);
for (let i = 0; i < folderMatch.fileCount() && numMatches < maxMatches; i++) {
const fileResult = fileMatchToString(matches[i], maxMatches - numMatches);
numMatches += fileResult.count;
fileResults.push(fileResult.text);
}
return {
text: fileResults.join(lineDelimiter + lineDelimiter),
count: numMatches
};
}
const maxClipboardMatches = 1e4;
export const copyMatchCommand: ICommandHandler = (accessor, match: RenderableMatch) => {
const clipboardService = accessor.get(IClipboardService);
let text: string | undefined;
if (match instanceof Match) {
text = matchToString(match);
} else if (match instanceof FileMatch) {
text = fileMatchToString(match, maxClipboardMatches).text;
} else if (match instanceof BaseFolderMatch) {
text = folderMatchToString(match, maxClipboardMatches).text;
}
if (text) {
clipboardService.writeText(text);
}
};
function allFolderMatchesToString(folderMatches: Array<FolderMatch | BaseFolderMatch>, maxMatches: number): string {
const folderResults: string[] = [];
let numMatches = 0;
folderMatches = folderMatches.sort(searchMatchComparer);
for (let i = 0; i < folderMatches.length && numMatches < maxMatches; i++) {
const folderResult = folderMatchToString(folderMatches[i], maxMatches - numMatches);
if (folderResult.count) {
numMatches += folderResult.count;
folderResults.push(folderResult.text);
}
}
return folderResults.join(lineDelimiter + lineDelimiter);
}
export const copyAllCommand: ICommandHandler = accessor => {
const viewletService = accessor.get(IViewletService);
const panelService = accessor.get(IPanelService);
const clipboardService = accessor.get(IClipboardService);
const searchView = getSearchView(viewletService, panelService);
if (searchView) {
const root = searchView.searchResult;
const text = allFolderMatchesToString(root.folderMatches(), maxClipboardMatches);
clipboardService.writeText(text);
}
};
export const clearHistoryCommand: ICommandHandler = accessor => {
const searchHistoryService = accessor.get(ISearchHistoryService);
searchHistoryService.clearHistory();
};
export const focusSearchListCommand: ICommandHandler = accessor => {
const viewletService = accessor.get(IViewletService);
const panelService = accessor.get(IPanelService);
const configurationService = accessor.get(IConfigurationService);
openSearchView(viewletService, panelService, configurationService).then(searchView => {
searchView.moveFocusToResults();
});
};

View File

@@ -0,0 +1,70 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IStorageService } from 'vs/platform/storage/common/storage';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { PANEL_ID } from 'vs/workbench/services/search/common/search';
import { SearchView } from 'vs/workbench/contrib/search/browser/searchView';
import { Panel } from 'vs/workbench/browser/panel';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { localize } from 'vs/nls';
import * as dom from 'vs/base/browser/dom';
import { IAction } from 'vs/base/common/actions';
export class SearchPanel extends Panel {
private readonly searchView: SearchView;
constructor(
@ITelemetryService telemetryService: ITelemetryService,
@IThemeService themeService: IThemeService,
@IStorageService storageService: IStorageService,
@IInstantiationService instantiationService: IInstantiationService,
) {
super(PANEL_ID, telemetryService, themeService, storageService);
this.searchView = this._register(instantiationService.createInstance(SearchView, { id: PANEL_ID, title: localize('search', "Search") }));
this._register(this.searchView.onDidChangeTitleArea(() => this.updateTitleArea()));
this._register(this.onDidChangeVisibility(visible => this.searchView.setVisible(visible)));
}
create(parent: HTMLElement): void {
dom.addClass(parent, 'monaco-panel-view');
this.searchView.render();
dom.append(parent, this.searchView.element);
this.searchView.setExpanded(true);
this.searchView.headerVisible = false;
}
public getTitle(): string {
return this.searchView.title;
}
public layout(dimension: dom.Dimension): void {
this.searchView.width = dimension.width;
this.searchView.layout(dimension.height);
}
public focus(): void {
this.searchView.focus();
}
getActions(): IAction[] {
return this.searchView.getActions();
}
getSecondaryActions(): IAction[] {
return this.searchView.getSecondaryActions();
}
saveState(): void {
this.searchView.saveState();
super.saveState();
}
getSearchView(): SearchView | null {
return this.searchView;
}
}

View File

@@ -0,0 +1,383 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as DOM from 'vs/base/browser/dom';
import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar';
import { CountBadge } from 'vs/base/browser/ui/countBadge/countBadge';
import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list';
import { IAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget';
import { ITreeNode, ITreeRenderer, ITreeDragAndDrop, ITreeDragOverReaction } from 'vs/base/browser/ui/tree/tree';
import { IAction } from 'vs/base/common/actions';
import { Disposable, IDisposable, dispose } from 'vs/base/common/lifecycle';
import * as paths from 'vs/base/common/path';
import * as resources from 'vs/base/common/resources';
import * as nls from 'vs/nls';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { FileKind } from 'vs/platform/files/common/files';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ILabelService } from 'vs/platform/label/common/label';
import { ISearchConfigurationProperties } from 'vs/workbench/services/search/common/search';
import { attachBadgeStyler } from 'vs/platform/theme/common/styler';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { IResourceLabel, ResourceLabels } from 'vs/workbench/browser/labels';
import { RemoveAction, ReplaceAction, ReplaceAllAction, ReplaceAllInFolderAction } from 'vs/workbench/contrib/search/browser/searchActions';
import { SearchView } from 'vs/workbench/contrib/search/browser/searchView';
import { FileMatch, FolderMatch, Match, RenderableMatch, SearchModel, BaseFolderMatch } from 'vs/workbench/contrib/search/common/searchModel';
import { IDragAndDropData } from 'vs/base/browser/dnd';
import { fillResourceDataTransfers } from 'vs/workbench/browser/dnd';
import { ElementsDragAndDropData } from 'vs/base/browser/ui/list/listView';
import { URI } from 'vs/base/common/uri';
interface IFolderMatchTemplate {
label: IResourceLabel;
badge: CountBadge;
actions: ActionBar;
disposables: IDisposable[];
}
interface IFileMatchTemplate {
el: HTMLElement;
label: IResourceLabel;
badge: CountBadge;
actions: ActionBar;
disposables: IDisposable[];
}
interface IMatchTemplate {
parent: HTMLElement;
before: HTMLElement;
match: HTMLElement;
replace: HTMLElement;
after: HTMLElement;
lineNumber: HTMLElement;
actions: ActionBar;
}
export class SearchDelegate implements IListVirtualDelegate<RenderableMatch> {
getHeight(element: RenderableMatch): number {
return 22;
}
getTemplateId(element: RenderableMatch): string {
if (element instanceof BaseFolderMatch) {
return FolderMatchRenderer.TEMPLATE_ID;
} else if (element instanceof FileMatch) {
return FileMatchRenderer.TEMPLATE_ID;
} else if (element instanceof Match) {
return MatchRenderer.TEMPLATE_ID;
}
console.error('Invalid search tree element', element);
throw new Error('Invalid search tree element');
}
}
export class FolderMatchRenderer extends Disposable implements ITreeRenderer<FolderMatch, any, IFolderMatchTemplate> {
static readonly TEMPLATE_ID = 'folderMatch';
readonly templateId = FolderMatchRenderer.TEMPLATE_ID;
constructor(
private searchModel: SearchModel,
private searchView: SearchView,
private labels: ResourceLabels,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IThemeService private readonly themeService: IThemeService,
@IWorkspaceContextService protected contextService: IWorkspaceContextService
) {
super();
}
renderTemplate(container: HTMLElement): IFolderMatchTemplate {
const disposables: IDisposable[] = [];
const folderMatchElement = DOM.append(container, DOM.$('.foldermatch'));
const label = this.labels.create(folderMatchElement);
disposables.push(label);
const badge = new CountBadge(DOM.append(folderMatchElement, DOM.$('.badge')));
disposables.push(attachBadgeStyler(badge, this.themeService));
const actionBarContainer = DOM.append(folderMatchElement, DOM.$('.actionBarContainer'));
const actions = new ActionBar(actionBarContainer, { animated: false });
disposables.push(actions);
return {
label,
badge,
actions,
disposables
};
}
renderElement(node: ITreeNode<FolderMatch, any>, index: number, templateData: IFolderMatchTemplate): void {
const folderMatch = node.element;
if (folderMatch.hasResource()) {
const workspaceFolder = this.contextService.getWorkspaceFolder(folderMatch.resource());
if (workspaceFolder && resources.isEqual(workspaceFolder.uri, folderMatch.resource())) {
templateData.label.setFile(folderMatch.resource(), { fileKind: FileKind.ROOT_FOLDER, hidePath: true });
} else {
templateData.label.setFile(folderMatch.resource(), { fileKind: FileKind.FOLDER });
}
} else {
templateData.label.setLabel(nls.localize('searchFolderMatch.other.label', "Other files"));
}
const count = folderMatch.fileCount();
templateData.badge.setCount(count);
templateData.badge.setTitleFormat(count > 1 ? nls.localize('searchFileMatches', "{0} files found", count) : nls.localize('searchFileMatch', "{0} file found", count));
templateData.actions.clear();
const actions: IAction[] = [];
if (this.searchModel.isReplaceActive() && count > 0) {
actions.push(this.instantiationService.createInstance(ReplaceAllInFolderAction, this.searchView.getControl(), folderMatch));
}
actions.push(new RemoveAction(this.searchView.getControl(), folderMatch));
templateData.actions.push(actions, { icon: true, label: false });
}
disposeElement(element: ITreeNode<RenderableMatch, any>, index: number, templateData: IFolderMatchTemplate): void {
}
disposeTemplate(templateData: IFolderMatchTemplate): void {
dispose(templateData.disposables);
}
}
export class FileMatchRenderer extends Disposable implements ITreeRenderer<FileMatch, any, IFileMatchTemplate> {
static readonly TEMPLATE_ID = 'fileMatch';
readonly templateId = FileMatchRenderer.TEMPLATE_ID;
constructor(
private searchModel: SearchModel,
private searchView: SearchView,
private labels: ResourceLabels,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IThemeService private readonly themeService: IThemeService,
@IWorkspaceContextService protected contextService: IWorkspaceContextService
) {
super();
}
renderTemplate(container: HTMLElement): IFileMatchTemplate {
const disposables: IDisposable[] = [];
const fileMatchElement = DOM.append(container, DOM.$('.filematch'));
const label = this.labels.create(fileMatchElement);
disposables.push(label);
const badge = new CountBadge(DOM.append(fileMatchElement, DOM.$('.badge')));
disposables.push(attachBadgeStyler(badge, this.themeService));
const actionBarContainer = DOM.append(fileMatchElement, DOM.$('.actionBarContainer'));
const actions = new ActionBar(actionBarContainer, { animated: false });
disposables.push(actions);
return {
el: fileMatchElement,
label,
badge,
actions,
disposables
};
}
renderElement(node: ITreeNode<FileMatch, any>, index: number, templateData: IFileMatchTemplate): void {
const fileMatch = node.element;
templateData.el.setAttribute('data-resource', fileMatch.resource().toString());
templateData.label.setFile(fileMatch.resource(), { hideIcon: false });
const count = fileMatch.count();
templateData.badge.setCount(count);
templateData.badge.setTitleFormat(count > 1 ? nls.localize('searchMatches', "{0} matches found", count) : nls.localize('searchMatch', "{0} match found", count));
templateData.actions.clear();
const actions: IAction[] = [];
if (this.searchModel.isReplaceActive() && count > 0) {
actions.push(this.instantiationService.createInstance(ReplaceAllAction, this.searchView, fileMatch));
}
actions.push(new RemoveAction(this.searchView.getControl(), fileMatch));
templateData.actions.push(actions, { icon: true, label: false });
}
disposeElement(element: ITreeNode<RenderableMatch, any>, index: number, templateData: IFileMatchTemplate): void {
}
disposeTemplate(templateData: IFileMatchTemplate): void {
dispose(templateData.disposables);
}
}
export class MatchRenderer extends Disposable implements ITreeRenderer<Match, void, IMatchTemplate> {
static readonly TEMPLATE_ID = 'match';
readonly templateId = MatchRenderer.TEMPLATE_ID;
constructor(
private searchModel: SearchModel,
private searchView: SearchView,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IWorkspaceContextService protected contextService: IWorkspaceContextService,
@IConfigurationService private readonly configurationService: IConfigurationService,
) {
super();
}
renderTemplate(container: HTMLElement): IMatchTemplate {
DOM.addClass(container, 'linematch');
const parent = DOM.append(container, DOM.$('a.plain.match'));
const before = DOM.append(parent, DOM.$('span'));
const match = DOM.append(parent, DOM.$('span.findInFileMatch'));
const replace = DOM.append(parent, DOM.$('span.replaceMatch'));
const after = DOM.append(parent, DOM.$('span'));
const lineNumber = DOM.append(container, DOM.$('span.matchLineNum'));
const actionBarContainer = DOM.append(container, DOM.$('span.actionBarContainer'));
const actions = new ActionBar(actionBarContainer, { animated: false });
return {
parent,
before,
match,
replace,
after,
lineNumber,
actions
};
}
renderElement(node: ITreeNode<Match, any>, index: number, templateData: IMatchTemplate): void {
const match = node.element;
const preview = match.preview();
const replace = this.searchModel.isReplaceActive() && !!this.searchModel.replaceString;
templateData.before.textContent = preview.before;
templateData.match.textContent = preview.inside;
DOM.toggleClass(templateData.match, 'replace', replace);
templateData.replace.textContent = replace ? match.replaceString : '';
templateData.after.textContent = preview.after;
templateData.parent.title = (preview.before + (replace ? match.replaceString : preview.inside) + preview.after).trim().substr(0, 999);
const numLines = match.range().endLineNumber - match.range().startLineNumber;
const extraLinesStr = numLines > 0 ? `+${numLines}` : '';
const showLineNumbers = this.configurationService.getValue<ISearchConfigurationProperties>('search').showLineNumbers;
const lineNumberStr = showLineNumbers ? `:${match.range().startLineNumber}` : '';
DOM.toggleClass(templateData.lineNumber, 'show', (numLines > 0) || showLineNumbers);
templateData.lineNumber.textContent = lineNumberStr + extraLinesStr;
templateData.lineNumber.setAttribute('title', this.getMatchTitle(match, showLineNumbers));
templateData.actions.clear();
if (this.searchModel.isReplaceActive()) {
templateData.actions.push([this.instantiationService.createInstance(ReplaceAction, this.searchView.getControl(), match, this.searchView), new RemoveAction(this.searchView.getControl(), match)], { icon: true, label: false });
} else {
templateData.actions.push([new RemoveAction(this.searchView.getControl(), match)], { icon: true, label: false });
}
}
disposeElement(element: ITreeNode<Match, any>, index: number, templateData: IMatchTemplate): void {
}
disposeTemplate(templateData: IMatchTemplate): void {
templateData.actions.dispose();
}
private getMatchTitle(match: Match, showLineNumbers: boolean): string {
const startLine = match.range().startLineNumber;
const numLines = match.range().endLineNumber - match.range().startLineNumber;
const lineNumStr = showLineNumbers ?
nls.localize('lineNumStr', "From line {0}", startLine, numLines) + ' ' :
'';
const numLinesStr = numLines > 0 ?
'+ ' + nls.localize('numLinesStr', "{0} more lines", numLines) :
'';
return lineNumStr + numLinesStr;
}
}
export class SearchAccessibilityProvider implements IAccessibilityProvider<RenderableMatch> {
constructor(
private searchModel: SearchModel,
@ILabelService private readonly labelService: ILabelService
) {
}
getAriaLabel(element: RenderableMatch): string | null {
if (element instanceof BaseFolderMatch) {
return element.hasResource() ?
nls.localize('folderMatchAriaLabel', "{0} matches in folder root {1}, Search result", element.count(), element.name()) :
nls.localize('otherFilesAriaLabel', "{0} matches outside of the workspace, Search result", element.count());
}
if (element instanceof FileMatch) {
const path = this.labelService.getUriLabel(element.resource(), { relative: true }) || element.resource().fsPath;
return nls.localize('fileMatchAriaLabel', "{0} matches in file {1} of folder {2}, Search result", element.count(), element.name(), paths.dirname(path));
}
if (element instanceof Match) {
const match = <Match>element;
const searchModel: SearchModel = this.searchModel;
const replace = searchModel.isReplaceActive() && !!searchModel.replaceString;
const matchString = match.getMatchString();
const range = match.range();
const matchText = match.text().substr(0, range.endColumn + 150);
if (replace) {
return nls.localize('replacePreviewResultAria', "Replace term {0} with {1} at column position {2} in line with text {3}", matchString, match.replaceString, range.startColumn + 1, matchText);
}
return nls.localize('searchResultAria', "Found term {0} at column position {1} in line with text {2}", matchString, range.startColumn + 1, matchText);
}
return null;
}
}
export class SearchDND implements ITreeDragAndDrop<RenderableMatch> {
constructor(
@IInstantiationService private instantiationService: IInstantiationService
) { }
onDragOver(data: IDragAndDropData, targetElement: RenderableMatch, targetIndex: number, originalEvent: DragEvent): boolean | ITreeDragOverReaction {
return false;
}
getDragURI(element: RenderableMatch): string | null {
if (element instanceof FileMatch) {
return element.remove.toString();
}
return null;
}
getDragLabel?(elements: RenderableMatch[]): string | undefined {
if (elements.length > 1) {
return String(elements.length);
}
const element = elements[0];
return element instanceof FileMatch ?
resources.basename(element.resource()) :
undefined;
}
onDragStart(data: IDragAndDropData, originalEvent: DragEvent): void {
const elements = (data as ElementsDragAndDropData<RenderableMatch>).elements;
const resources: URI[] = elements
.filter(e => e instanceof FileMatch)
.map((fm: FileMatch) => fm.resource());
if (resources.length) {
// Apply some datatransfer types to allow for dragging the element outside of the application
this.instantiationService.invokeFunction(fillResourceDataTransfers, resources, originalEvent);
}
}
drop(data: IDragAndDropData, targetElement: RenderableMatch, targetIndex: number, originalEvent: DragEvent): void {
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,45 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ViewContainerViewlet } from 'vs/workbench/browser/parts/views/viewsViewlet';
import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { IStorageService } from 'vs/platform/storage/common/storage';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
import { VIEWLET_ID, VIEW_ID } from 'vs/workbench/services/search/common/search';
import { SearchView } from 'vs/workbench/contrib/search/browser/searchView';
import { Registry } from 'vs/platform/registry/common/platform';
import { ViewletRegistry, Extensions } from 'vs/workbench/browser/viewlet';
export class SearchViewlet extends ViewContainerViewlet {
constructor(
@IWorkbenchLayoutService layoutService: IWorkbenchLayoutService,
@ITelemetryService telemetryService: ITelemetryService,
@IWorkspaceContextService protected contextService: IWorkspaceContextService,
@IStorageService protected storageService: IStorageService,
@IConfigurationService configurationService: IConfigurationService,
@IInstantiationService protected instantiationService: IInstantiationService,
@IThemeService themeService: IThemeService,
@IContextMenuService contextMenuService: IContextMenuService,
@IExtensionService extensionService: IExtensionService
) {
super(VIEWLET_ID, `${VIEWLET_ID}.state`, true, configurationService, layoutService, telemetryService, storageService, instantiationService, themeService, contextMenuService, extensionService, contextService);
}
getTitle(): string {
return Registry.as<ViewletRegistry>(Extensions.Viewlets).getViewlet(this.getId()).name;
}
getSearchView(): SearchView | null {
const view = super.getView(VIEW_ID);
return view ? view as SearchView : null;
}
}

View File

@@ -0,0 +1,557 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as dom from 'vs/base/browser/dom';
import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar';
import { Button, IButtonOptions } from 'vs/base/browser/ui/button/button';
import { FindInput, IFindInputOptions } from 'vs/base/browser/ui/findinput/findInput';
import { HistoryInputBox, IMessage } from 'vs/base/browser/ui/inputbox/inputBox';
import { Widget } from 'vs/base/browser/ui/widget';
import { Action } from 'vs/base/common/actions';
import { Delayer } from 'vs/base/common/async';
import { Emitter, Event } from 'vs/base/common/event';
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
import * as strings from 'vs/base/common/strings';
import { CONTEXT_FIND_WIDGET_NOT_VISIBLE } from 'vs/editor/contrib/find/findModel';
import * as nls from 'vs/nls';
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { ContextKeyExpr, IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
import { ISearchConfigurationProperties } from 'vs/workbench/services/search/common/search';
import { attachFindInputBoxStyler, attachInputBoxStyler } from 'vs/platform/theme/common/styler';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { ContextScopedFindInput, ContextScopedHistoryInputBox } from 'vs/platform/browser/contextScopedHistoryWidget';
import { appendKeyBindingLabel, isSearchViewFocused } from 'vs/workbench/contrib/search/browser/searchActions';
import * as Constants from 'vs/workbench/contrib/search/common/constants';
import { IPanelService } from 'vs/workbench/services/panel/common/panelService';
import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet';
import { IEditorOptions } from 'vs/editor/common/config/editorOptions';
import { IAccessibilityService, AccessibilitySupport } from 'vs/platform/accessibility/common/accessibility';
export interface ISearchWidgetOptions {
value?: string;
replaceValue?: string;
isRegex?: boolean;
isCaseSensitive?: boolean;
isWholeWords?: boolean;
searchHistory?: string[];
replaceHistory?: string[];
}
class ReplaceAllAction extends Action {
private static fgInstance: ReplaceAllAction | null = null;
static readonly ID: string = 'search.action.replaceAll';
static get INSTANCE(): ReplaceAllAction {
if (ReplaceAllAction.fgInstance === null) {
ReplaceAllAction.fgInstance = new ReplaceAllAction();
}
return ReplaceAllAction.fgInstance;
}
private _searchWidget: SearchWidget | null = null;
constructor() {
super(ReplaceAllAction.ID, '', 'action-replace-all', false);
}
set searchWidget(searchWidget: SearchWidget) {
this._searchWidget = searchWidget;
}
run(): Promise<any> {
if (this._searchWidget) {
return this._searchWidget.triggerReplaceAll();
}
return Promise.resolve(null);
}
}
export class SearchWidget extends Widget {
private static readonly REPLACE_ALL_DISABLED_LABEL = nls.localize('search.action.replaceAll.disabled.label', "Replace All (Submit Search to Enable)");
private static readonly REPLACE_ALL_ENABLED_LABEL = (keyBindingService2: IKeybindingService): string => {
const kb = keyBindingService2.lookupKeybinding(ReplaceAllAction.ID);
return appendKeyBindingLabel(nls.localize('search.action.replaceAll.enabled.label', "Replace All"), kb, keyBindingService2);
}
domNode: HTMLElement;
searchInput: FindInput;
searchInputFocusTracker: dom.IFocusTracker;
private searchInputBoxFocused: IContextKey<boolean>;
private replaceContainer: HTMLElement;
replaceInput: HistoryInputBox;
private toggleReplaceButton: Button;
private replaceAllAction: ReplaceAllAction;
private replaceActive: IContextKey<boolean>;
private replaceActionBar: ActionBar;
replaceInputFocusTracker: dom.IFocusTracker;
private replaceInputBoxFocused: IContextKey<boolean>;
private _replaceHistoryDelayer: Delayer<void>;
private ignoreGlobalFindBufferOnNextFocus = false;
private previousGlobalFindBufferValue: string;
private _onSearchSubmit = this._register(new Emitter<void>());
readonly onSearchSubmit: Event<void> = this._onSearchSubmit.event;
private _onSearchCancel = this._register(new Emitter<void>());
readonly onSearchCancel: Event<void> = this._onSearchCancel.event;
private _onReplaceToggled = this._register(new Emitter<void>());
readonly onReplaceToggled: Event<void> = this._onReplaceToggled.event;
private _onReplaceStateChange = this._register(new Emitter<boolean>());
readonly onReplaceStateChange: Event<boolean> = this._onReplaceStateChange.event;
private _onReplaceValueChanged = this._register(new Emitter<string | undefined>());
readonly onReplaceValueChanged: Event<string> = this._onReplaceValueChanged.event;
private _onReplaceAll = this._register(new Emitter<void>());
readonly onReplaceAll: Event<void> = this._onReplaceAll.event;
private _onBlur = this._register(new Emitter<void>());
readonly onBlur: Event<void> = this._onBlur.event;
private _onDidHeightChange = this._register(new Emitter<void>());
readonly onDidHeightChange: Event<void> = this._onDidHeightChange.event;
constructor(
container: HTMLElement,
options: ISearchWidgetOptions,
@IContextViewService private readonly contextViewService: IContextViewService,
@IThemeService private readonly themeService: IThemeService,
@IContextKeyService private readonly contextKeyService: IContextKeyService,
@IKeybindingService private readonly keyBindingService: IKeybindingService,
@IClipboardService private readonly clipboardServce: IClipboardService,
@IConfigurationService private readonly configurationService: IConfigurationService,
@IAccessibilityService private readonly accessibilityService: IAccessibilityService
) {
super();
this.replaceActive = Constants.ReplaceActiveKey.bindTo(this.contextKeyService);
this.searchInputBoxFocused = Constants.SearchInputBoxFocusedKey.bindTo(this.contextKeyService);
this.replaceInputBoxFocused = Constants.ReplaceInputBoxFocusedKey.bindTo(this.contextKeyService);
this._replaceHistoryDelayer = new Delayer<void>(500);
this.render(container, options);
this.configurationService.onDidChangeConfiguration(e => {
if (e.affectsConfiguration('editor.accessibilitySupport')) {
this.updateAccessibilitySupport();
}
});
this.accessibilityService.onDidChangeAccessibilitySupport(() => this.updateAccessibilitySupport());
this.updateAccessibilitySupport();
}
focus(select: boolean = true, focusReplace: boolean = false, suppressGlobalSearchBuffer = false): void {
this.ignoreGlobalFindBufferOnNextFocus = suppressGlobalSearchBuffer;
if (focusReplace && this.isReplaceShown()) {
this.replaceInput.focus();
if (select) {
this.replaceInput.select();
}
} else {
this.searchInput.focus();
if (select) {
this.searchInput.select();
}
}
}
setWidth(width: number) {
this.searchInput.inputBox.layout();
this.replaceInput.width = width - 28;
this.replaceInput.layout();
}
clear() {
this.searchInput.clear();
this.replaceInput.value = '';
this.setReplaceAllActionState(false);
}
isReplaceShown(): boolean {
return !dom.hasClass(this.replaceContainer, 'disabled');
}
isReplaceActive(): boolean {
return !!this.replaceActive.get();
}
getReplaceValue(): string {
return this.replaceInput.value;
}
toggleReplace(show?: boolean): void {
if (show === undefined || show !== this.isReplaceShown()) {
this.onToggleReplaceButton();
}
}
getSearchHistory(): string[] {
return this.searchInput.inputBox.getHistory();
}
getReplaceHistory(): string[] {
return this.replaceInput.getHistory();
}
clearHistory(): void {
this.searchInput.inputBox.clearHistory();
}
showNextSearchTerm() {
this.searchInput.inputBox.showNextValue();
}
showPreviousSearchTerm() {
this.searchInput.inputBox.showPreviousValue();
}
showNextReplaceTerm() {
this.replaceInput.showNextValue();
}
showPreviousReplaceTerm() {
this.replaceInput.showPreviousValue();
}
searchInputHasFocus(): boolean {
return !!this.searchInputBoxFocused.get();
}
replaceInputHasFocus(): boolean {
return this.replaceInput.hasFocus();
}
focusReplaceAllAction(): void {
this.replaceActionBar.focus(true);
}
focusRegexAction(): void {
this.searchInput.focusOnRegex();
}
private render(container: HTMLElement, options: ISearchWidgetOptions): void {
this.domNode = dom.append(container, dom.$('.search-widget'));
this.domNode.style.position = 'relative';
this.renderToggleReplaceButton(this.domNode);
this.renderSearchInput(this.domNode, options);
this.renderReplaceInput(this.domNode, options);
}
private isScreenReaderOptimized() {
const detected = this.accessibilityService.getAccessibilitySupport() === AccessibilitySupport.Enabled;
const config = this.configurationService.getValue<IEditorOptions>('editor').accessibilitySupport;
return config === 'on' || (config === 'auto' && detected);
}
private updateAccessibilitySupport(): void {
this.searchInput.setFocusInputOnOptionClick(!this.isScreenReaderOptimized());
}
private renderToggleReplaceButton(parent: HTMLElement): void {
const opts: IButtonOptions = {
buttonBackground: undefined,
buttonBorder: undefined,
buttonForeground: undefined,
buttonHoverBackground: undefined
};
this.toggleReplaceButton = this._register(new Button(parent, opts));
this.toggleReplaceButton.element.setAttribute('aria-expanded', 'false');
this.toggleReplaceButton.element.classList.add('collapse');
this.toggleReplaceButton.icon = 'toggle-replace-button';
// TODO@joh need to dispose this listener eventually
this.toggleReplaceButton.onDidClick(() => this.onToggleReplaceButton());
this.toggleReplaceButton.element.title = nls.localize('search.replace.toggle.button.title', "Toggle Replace");
}
private renderSearchInput(parent: HTMLElement, options: ISearchWidgetOptions): void {
const inputOptions: IFindInputOptions = {
label: nls.localize('label.Search', 'Search: Type Search Term and press Enter to search or Escape to cancel'),
validation: (value: string) => this.validateSearchInput(value),
placeholder: nls.localize('search.placeHolder', "Search"),
appendCaseSensitiveLabel: appendKeyBindingLabel('', this.keyBindingService.lookupKeybinding(Constants.ToggleCaseSensitiveCommandId), this.keyBindingService),
appendWholeWordsLabel: appendKeyBindingLabel('', this.keyBindingService.lookupKeybinding(Constants.ToggleWholeWordCommandId), this.keyBindingService),
appendRegexLabel: appendKeyBindingLabel('', this.keyBindingService.lookupKeybinding(Constants.ToggleRegexCommandId), this.keyBindingService),
history: options.searchHistory,
flexibleHeight: true
};
const searchInputContainer = dom.append(parent, dom.$('.search-container.input-box'));
this.searchInput = this._register(new ContextScopedFindInput(searchInputContainer, this.contextViewService, inputOptions, this.contextKeyService, true));
this._register(attachFindInputBoxStyler(this.searchInput, this.themeService));
this.searchInput.onKeyDown((keyboardEvent: IKeyboardEvent) => this.onSearchInputKeyDown(keyboardEvent));
this.searchInput.setValue(options.value || '');
this.searchInput.setRegex(!!options.isRegex);
this.searchInput.setCaseSensitive(!!options.isCaseSensitive);
this.searchInput.setWholeWords(!!options.isWholeWords);
this._register(this.onSearchSubmit(() => {
this.searchInput.inputBox.addToHistory();
}));
this._register(this.searchInput.onCaseSensitiveKeyDown((keyboardEvent: IKeyboardEvent) => this.onCaseSensitiveKeyDown(keyboardEvent)));
this._register(this.searchInput.onRegexKeyDown((keyboardEvent: IKeyboardEvent) => this.onRegexKeyDown(keyboardEvent)));
this._register(this.searchInput.inputBox.onDidChange(() => this.onSearchInputChanged()));
this._register(this.searchInput.inputBox.onDidHeightChange(() => this._onDidHeightChange.fire()));
this._register(this.onReplaceValueChanged(() => {
this._replaceHistoryDelayer.trigger(() => this.replaceInput.addToHistory());
}));
this.searchInputFocusTracker = this._register(dom.trackFocus(this.searchInput.inputBox.inputElement));
this._register(this.searchInputFocusTracker.onDidFocus(() => {
this.searchInputBoxFocused.set(true);
const useGlobalFindBuffer = this.searchConfiguration.globalFindClipboard;
if (!this.ignoreGlobalFindBufferOnNextFocus && useGlobalFindBuffer) {
const globalBufferText = this.clipboardServce.readFindText();
if (this.previousGlobalFindBufferValue !== globalBufferText) {
this.searchInput.inputBox.addToHistory();
this.searchInput.setValue(globalBufferText);
this.searchInput.select();
}
this.previousGlobalFindBufferValue = globalBufferText;
}
this.ignoreGlobalFindBufferOnNextFocus = false;
}));
this._register(this.searchInputFocusTracker.onDidBlur(() => this.searchInputBoxFocused.set(false)));
}
private renderReplaceInput(parent: HTMLElement, options: ISearchWidgetOptions): void {
this.replaceContainer = dom.append(parent, dom.$('.replace-container.disabled'));
const replaceBox = dom.append(this.replaceContainer, dom.$('.input-box'));
this.replaceInput = this._register(new ContextScopedHistoryInputBox(replaceBox, this.contextViewService, {
ariaLabel: nls.localize('label.Replace', 'Replace: Type replace term and press Enter to preview or Escape to cancel'),
placeholder: nls.localize('search.replace.placeHolder', "Replace"),
history: options.replaceHistory || [],
flexibleHeight: true
}, this.contextKeyService));
this._register(attachInputBoxStyler(this.replaceInput, this.themeService));
this.onkeydown(this.replaceInput.inputElement, (keyboardEvent) => this.onReplaceInputKeyDown(keyboardEvent));
this.replaceInput.value = options.replaceValue || '';
this._register(this.replaceInput.onDidChange(() => this._onReplaceValueChanged.fire(undefined)));
this._register(this.replaceInput.onDidHeightChange(() => this._onDidHeightChange.fire()));
this.replaceAllAction = ReplaceAllAction.INSTANCE;
this.replaceAllAction.searchWidget = this;
this.replaceAllAction.label = SearchWidget.REPLACE_ALL_DISABLED_LABEL;
this.replaceActionBar = this._register(new ActionBar(this.replaceContainer));
this.replaceActionBar.push([this.replaceAllAction], { icon: true, label: false });
this.onkeydown(this.replaceActionBar.domNode, (keyboardEvent) => this.onReplaceActionbarKeyDown(keyboardEvent));
this.replaceInputFocusTracker = this._register(dom.trackFocus(this.replaceInput.inputElement));
this._register(this.replaceInputFocusTracker.onDidFocus(() => this.replaceInputBoxFocused.set(true)));
this._register(this.replaceInputFocusTracker.onDidBlur(() => this.replaceInputBoxFocused.set(false)));
}
triggerReplaceAll(): Promise<any> {
this._onReplaceAll.fire();
return Promise.resolve(null);
}
private onToggleReplaceButton(): void {
dom.toggleClass(this.replaceContainer, 'disabled');
dom.toggleClass(this.toggleReplaceButton.element, 'collapse');
dom.toggleClass(this.toggleReplaceButton.element, 'expand');
this.toggleReplaceButton.element.setAttribute('aria-expanded', this.isReplaceShown() ? 'true' : 'false');
this.updateReplaceActiveState();
this._onReplaceToggled.fire();
}
setReplaceAllActionState(enabled: boolean): void {
if (this.replaceAllAction.enabled !== enabled) {
this.replaceAllAction.enabled = enabled;
this.replaceAllAction.label = enabled ? SearchWidget.REPLACE_ALL_ENABLED_LABEL(this.keyBindingService) : SearchWidget.REPLACE_ALL_DISABLED_LABEL;
this.updateReplaceActiveState();
}
}
private updateReplaceActiveState(): void {
const currentState = this.isReplaceActive();
const newState = this.isReplaceShown() && this.replaceAllAction.enabled;
if (currentState !== newState) {
this.replaceActive.set(newState);
this._onReplaceStateChange.fire(newState);
this.replaceInput.layout();
}
}
private validateSearchInput(value: string): IMessage | null {
if (value.length === 0) {
return null;
}
if (!this.searchInput.getRegex()) {
return null;
}
try {
// tslint:disable-next-line: no-unused-expression
new RegExp(value);
} catch (e) {
return { content: e.message };
}
if (strings.regExpContainsBackreference(value)) {
if (!this.searchConfiguration.usePCRE2) {
return { content: nls.localize('regexp.backreferenceValidationFailure', "Backreferences are not supported") };
}
}
return null;
}
private onSearchInputChanged(): void {
this.searchInput.clearMessage();
this.setReplaceAllActionState(false);
}
private onSearchInputKeyDown(keyboardEvent: IKeyboardEvent) {
if (keyboardEvent.equals(KeyCode.Enter)) {
this.submitSearch();
keyboardEvent.preventDefault();
}
else if (keyboardEvent.equals(KeyCode.Escape)) {
this._onSearchCancel.fire();
keyboardEvent.preventDefault();
}
else if (keyboardEvent.equals(KeyCode.Tab)) {
if (this.isReplaceShown()) {
this.replaceInput.focus();
} else {
this.searchInput.focusOnCaseSensitive();
}
keyboardEvent.preventDefault();
}
else if (keyboardEvent.equals(KeyCode.UpArrow)) {
const ta = this.searchInput.domNode.querySelector('textarea');
const isMultiline = !!this.searchInput.getValue().match(/\n/);
if (ta && isMultiline && ta.selectionStart > 0) {
keyboardEvent.stopPropagation();
}
}
else if (keyboardEvent.equals(KeyCode.DownArrow)) {
const ta = this.searchInput.domNode.querySelector('textarea');
const isMultiline = !!this.searchInput.getValue().match(/\n/);
if (ta && isMultiline && ta.selectionEnd < ta.value.length) {
keyboardEvent.stopPropagation();
}
}
}
private onCaseSensitiveKeyDown(keyboardEvent: IKeyboardEvent) {
if (keyboardEvent.equals(KeyMod.Shift | KeyCode.Tab)) {
if (this.isReplaceShown()) {
this.replaceInput.focus();
keyboardEvent.preventDefault();
}
}
}
private onRegexKeyDown(keyboardEvent: IKeyboardEvent) {
if (keyboardEvent.equals(KeyCode.Tab)) {
if (this.isReplaceActive()) {
this.focusReplaceAllAction();
} else {
this._onBlur.fire();
}
keyboardEvent.preventDefault();
}
}
private onReplaceInputKeyDown(keyboardEvent: IKeyboardEvent) {
if (keyboardEvent.equals(KeyCode.Enter)) {
this.submitSearch();
keyboardEvent.preventDefault();
}
else if (keyboardEvent.equals(KeyCode.Tab)) {
this.searchInput.focusOnCaseSensitive();
keyboardEvent.preventDefault();
}
else if (keyboardEvent.equals(KeyMod.Shift | KeyCode.Tab)) {
this.searchInput.focus();
keyboardEvent.preventDefault();
}
else if (keyboardEvent.equals(KeyCode.UpArrow)) {
const ta = this.searchInput.domNode.querySelector('textarea');
if (ta && ta.selectionStart > 0) {
keyboardEvent.stopPropagation();
}
}
else if (keyboardEvent.equals(KeyCode.DownArrow)) {
const ta = this.searchInput.domNode.querySelector('textarea');
if (ta && ta.selectionEnd < ta.value.length) {
keyboardEvent.stopPropagation();
}
}
}
private onReplaceActionbarKeyDown(keyboardEvent: IKeyboardEvent) {
if (keyboardEvent.equals(KeyMod.Shift | KeyCode.Tab)) {
this.focusRegexAction();
keyboardEvent.preventDefault();
}
}
private submitSearch(): void {
this.searchInput.validate();
if (!this.searchInput.inputBox.isInputValid()) {
return;
}
const value = this.searchInput.getValue();
const useGlobalFindBuffer = this.searchConfiguration.globalFindClipboard;
if (value) {
if (useGlobalFindBuffer) {
this.clipboardServce.writeFindText(value);
}
this._onSearchSubmit.fire();
}
}
dispose(): void {
this.setReplaceAllActionState(false);
this.replaceAllAction.searchWidget = null;
this.replaceActionBar = null;
super.dispose();
}
private get searchConfiguration(): ISearchConfigurationProperties {
return this.configurationService.getValue<ISearchConfigurationProperties>('search');
}
}
export function registerContributions() {
KeybindingsRegistry.registerCommandAndKeybindingRule({
id: ReplaceAllAction.ID,
weight: KeybindingWeight.WorkbenchContrib,
when: ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.ReplaceActiveKey, CONTEXT_FIND_WIDGET_NOT_VISIBLE),
primary: KeyMod.Alt | KeyMod.CtrlCmd | KeyCode.Enter,
handler: accessor => {
if (isSearchViewFocused(accessor.get(IViewletService), accessor.get(IPanelService))) {
ReplaceAllAction.INSTANCE.run();
}
}
});
}

View File

@@ -0,0 +1,45 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { RawContextKey } from 'vs/platform/contextkey/common/contextkey';
export const FindInFilesActionId = 'workbench.action.findInFiles';
export const FocusActiveEditorCommandId = 'search.action.focusActiveEditor';
export const FocusSearchFromResults = 'search.action.focusSearchFromResults';
export const OpenMatchToSide = 'search.action.openResultToSide';
export const CancelActionId = 'search.action.cancel';
export const RemoveActionId = 'search.action.remove';
export const CopyPathCommandId = 'search.action.copyPath';
export const CopyMatchCommandId = 'search.action.copyMatch';
export const CopyAllCommandId = 'search.action.copyAll';
export const ClearSearchHistoryCommandId = 'search.action.clearHistory';
export const FocusSearchListCommandID = 'search.action.focusSearchList';
export const ReplaceActionId = 'search.action.replace';
export const ReplaceAllInFileActionId = 'search.action.replaceAllInFile';
export const ReplaceAllInFolderActionId = 'search.action.replaceAllInFolder';
export const CloseReplaceWidgetActionId = 'closeReplaceInFilesWidget';
export const ToggleCaseSensitiveCommandId = 'toggleSearchCaseSensitive';
export const ToggleWholeWordCommandId = 'toggleSearchWholeWord';
export const ToggleRegexCommandId = 'toggleSearchRegex';
export const ToggleSearchViewPositionCommandId = 'search.action.toggleSearchViewPosition';
export const SearchViewVisibleKey = new RawContextKey<boolean>('searchViewletVisible', true);
export const SearchViewFocusedKey = new RawContextKey<boolean>('searchViewletFocus', false);
export const InputBoxFocusedKey = new RawContextKey<boolean>('inputBoxFocus', false);
export const SearchInputBoxFocusedKey = new RawContextKey<boolean>('searchInputBoxFocus', false);
export const ReplaceInputBoxFocusedKey = new RawContextKey<boolean>('replaceInputBoxFocus', false);
export const PatternIncludesFocusedKey = new RawContextKey<boolean>('patternIncludesInputBoxFocus', false);
export const PatternExcludesFocusedKey = new RawContextKey<boolean>('patternExcludesInputBoxFocus', false);
export const ReplaceActiveKey = new RawContextKey<boolean>('replaceActive', false);
export const HasSearchResults = new RawContextKey<boolean>('hasSearchResult', false);
export const FirstMatchFocusKey = new RawContextKey<boolean>('firstMatchFocus', false);
export const FileMatchOrMatchFocusKey = new RawContextKey<boolean>('fileMatchOrMatchFocus', false); // This is actually, Match or File or Folder
export const FileMatchOrFolderMatchFocusKey = new RawContextKey<boolean>('fileMatchOrFolderMatchFocus', false);
export const FileFocusKey = new RawContextKey<boolean>('fileMatchFocus', false);
export const FolderFocusKey = new RawContextKey<boolean>('folderMatchFocus', false);
export const MatchFocusKey = new RawContextKey<boolean>('matchFocus', false);

View File

@@ -0,0 +1,499 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as arrays from 'vs/base/common/arrays';
import * as collections from 'vs/base/common/collections';
import * as glob from 'vs/base/common/glob';
import { untildify } from 'vs/base/common/labels';
import { values } from 'vs/base/common/map';
import * as path from 'vs/base/common/path';
import { isEqual } from 'vs/base/common/resources';
import * as strings from 'vs/base/common/strings';
import { URI as uri } from 'vs/base/common/uri';
import { isMultilineRegexSource } from 'vs/editor/common/model/textModelSearch';
import * as nls from 'vs/nls';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace';
import { getExcludes, ICommonQueryProps, IFileQuery, IFolderQuery, IPatternInfo, ISearchConfiguration, ITextQuery, ITextSearchPreviewOptions, pathIncludedInQuery, QueryType } from 'vs/workbench/services/search/common/search';
/**
* One folder to search and a glob expression that should be applied.
*/
export interface IOneSearchPathPattern {
searchPath: uri;
pattern?: string;
}
/**
* One folder to search and a set of glob expressions that should be applied.
*/
export interface ISearchPathPattern {
searchPath: uri;
pattern?: glob.IExpression;
}
/**
* A set of search paths and a set of glob expressions that should be applied.
*/
export interface ISearchPathsInfo {
searchPaths?: ISearchPathPattern[];
pattern?: glob.IExpression;
}
export interface ICommonQueryBuilderOptions {
_reason?: string;
excludePattern?: string;
includePattern?: string;
extraFileResources?: uri[];
/** Parse the special ./ syntax supported by the searchview, and expand foo to ** /foo */
expandPatterns?: boolean;
maxResults?: number;
maxFileSize?: number;
disregardIgnoreFiles?: boolean;
disregardGlobalIgnoreFiles?: boolean;
disregardExcludeSettings?: boolean;
disregardSearchExcludeSettings?: boolean;
ignoreSymlinks?: boolean;
}
export interface IFileQueryBuilderOptions extends ICommonQueryBuilderOptions {
filePattern?: string;
exists?: boolean;
sortByScore?: boolean;
cacheKey?: string;
}
export interface ITextQueryBuilderOptions extends ICommonQueryBuilderOptions {
previewOptions?: ITextSearchPreviewOptions;
fileEncoding?: string;
beforeContext?: number;
afterContext?: number;
isSmartCase?: boolean;
}
export class QueryBuilder {
constructor(
@IConfigurationService private readonly configurationService: IConfigurationService,
@IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService,
@IEnvironmentService private readonly environmentService: IEnvironmentService
) { }
text(contentPattern: IPatternInfo, folderResources?: uri[], options: ITextQueryBuilderOptions = {}): ITextQuery {
contentPattern = this.getContentPattern(contentPattern, options);
const searchConfig = this.configurationService.getValue<ISearchConfiguration>();
const fallbackToPCRE = folderResources && folderResources.some(folder => {
const folderConfig = this.configurationService.getValue<ISearchConfiguration>({ resource: folder });
return !folderConfig.search.useRipgrep;
});
const commonQuery = this.commonQuery(folderResources, options);
return <ITextQuery>{
...commonQuery,
type: QueryType.Text,
contentPattern,
previewOptions: options.previewOptions,
maxFileSize: options.maxFileSize,
usePCRE2: searchConfig.search.usePCRE2 || fallbackToPCRE || false,
beforeContext: options.beforeContext,
afterContext: options.afterContext,
userDisabledExcludesAndIgnoreFiles: options.disregardExcludeSettings && options.disregardIgnoreFiles
};
}
/**
* Adjusts input pattern for config
*/
private getContentPattern(inputPattern: IPatternInfo, options: ITextQueryBuilderOptions): IPatternInfo {
const searchConfig = this.configurationService.getValue<ISearchConfiguration>();
if (inputPattern.isRegExp) {
inputPattern.pattern = inputPattern.pattern.replace(/\r?\n/g, '\\n');
}
const newPattern = {
...inputPattern,
wordSeparators: searchConfig.editor.wordSeparators
};
if (this.isCaseSensitive(inputPattern, options)) {
newPattern.isCaseSensitive = true;
}
if (this.isMultiline(inputPattern)) {
newPattern.isMultiline = true;
}
return newPattern;
}
file(folderResources: uri[] | undefined, options: IFileQueryBuilderOptions = {}): IFileQuery {
const commonQuery = this.commonQuery(folderResources, options);
return <IFileQuery>{
...commonQuery,
type: QueryType.File,
filePattern: options.filePattern
? options.filePattern.trim()
: options.filePattern,
exists: options.exists,
sortByScore: options.sortByScore,
cacheKey: options.cacheKey
};
}
private commonQuery(folderResources: uri[] = [], options: ICommonQueryBuilderOptions = {}): ICommonQueryProps<uri> {
let includeSearchPathsInfo: ISearchPathsInfo = {};
if (options.includePattern) {
includeSearchPathsInfo = options.expandPatterns ?
this.parseSearchPaths(options.includePattern) :
{ pattern: patternListToIExpression(options.includePattern) };
}
let excludeSearchPathsInfo: ISearchPathsInfo = {};
if (options.excludePattern) {
excludeSearchPathsInfo = options.expandPatterns ?
this.parseSearchPaths(options.excludePattern) :
{ pattern: patternListToIExpression(options.excludePattern) };
}
// Build folderQueries from searchPaths, if given, otherwise folderResources
const folderQueries = (includeSearchPathsInfo.searchPaths && includeSearchPathsInfo.searchPaths.length ?
includeSearchPathsInfo.searchPaths.map(searchPath => this.getFolderQueryForSearchPath(searchPath, options, excludeSearchPathsInfo)) :
folderResources.map(uri => this.getFolderQueryForRoot(uri, options, excludeSearchPathsInfo)))
.filter(query => !!query) as IFolderQuery[];
const queryProps: ICommonQueryProps<uri> = {
_reason: options._reason,
folderQueries,
usingSearchPaths: !!(includeSearchPathsInfo.searchPaths && includeSearchPathsInfo.searchPaths.length),
extraFileResources: options.extraFileResources,
excludePattern: excludeSearchPathsInfo.pattern,
includePattern: includeSearchPathsInfo.pattern,
maxResults: options.maxResults
};
// Filter extraFileResources against global include/exclude patterns - they are already expected to not belong to a workspace
const extraFileResources = options.extraFileResources && options.extraFileResources.filter(extraFile => pathIncludedInQuery(queryProps, extraFile.fsPath));
queryProps.extraFileResources = extraFileResources && extraFileResources.length ? extraFileResources : undefined;
return queryProps;
}
/**
* Resolve isCaseSensitive flag based on the query and the isSmartCase flag, for search providers that don't support smart case natively.
*/
private isCaseSensitive(contentPattern: IPatternInfo, options: ITextQueryBuilderOptions): boolean {
if (options.isSmartCase) {
if (contentPattern.isRegExp) {
// Consider it case sensitive if it contains an unescaped capital letter
if (strings.containsUppercaseCharacter(contentPattern.pattern, true)) {
return true;
}
} else if (strings.containsUppercaseCharacter(contentPattern.pattern)) {
return true;
}
}
return !!contentPattern.isCaseSensitive;
}
private isMultiline(contentPattern: IPatternInfo): boolean {
if (contentPattern.isMultiline) {
return true;
}
if (contentPattern.isRegExp && isMultilineRegexSource(contentPattern.pattern)) {
return true;
}
if (contentPattern.pattern.indexOf('\n') >= 0) {
return true;
}
return !!contentPattern.isMultiline;
}
/**
* Take the includePattern as seen in the search viewlet, and split into components that look like searchPaths, and
* glob patterns. Glob patterns are expanded from 'foo/bar' to '{foo/bar/**, **\/foo/bar}.
*
* Public for test.
*/
parseSearchPaths(pattern: string): ISearchPathsInfo {
const isSearchPath = (segment: string) => {
// A segment is a search path if it is an absolute path or starts with ./, ../, .\, or ..\
return path.isAbsolute(segment) || /^\.\.?[\/\\]/.test(segment);
};
const segments = splitGlobPattern(pattern)
.map(segment => untildify(segment, this.environmentService.userHome));
const groups = collections.groupBy(segments,
segment => isSearchPath(segment) ? 'searchPaths' : 'exprSegments');
const expandedExprSegments = (groups.exprSegments || [])
.map(s => strings.rtrim(s, '/'))
.map(s => strings.rtrim(s, '\\'))
.map(p => {
if (p[0] === '.') {
p = '*' + p; // convert ".js" to "*.js"
}
return expandGlobalGlob(p);
});
const result: ISearchPathsInfo = {};
const searchPaths = this.expandSearchPathPatterns(groups.searchPaths || []);
if (searchPaths && searchPaths.length) {
result.searchPaths = searchPaths;
}
const exprSegments = arrays.flatten(expandedExprSegments);
const includePattern = patternListToIExpression(...exprSegments);
if (includePattern) {
result.pattern = includePattern;
}
return result;
}
private getExcludesForFolder(folderConfig: ISearchConfiguration, options: ICommonQueryBuilderOptions): glob.IExpression | undefined {
return options.disregardExcludeSettings ?
undefined :
getExcludes(folderConfig, !options.disregardSearchExcludeSettings);
}
/**
* Split search paths (./ or ../ or absolute paths in the includePatterns) into absolute paths and globs applied to those paths
*/
private expandSearchPathPatterns(searchPaths: string[]): ISearchPathPattern[] {
if (this.workspaceContextService.getWorkbenchState() === WorkbenchState.EMPTY || !searchPaths || !searchPaths.length) {
// No workspace => ignore search paths
return [];
}
const expandedSearchPaths = arrays.flatten(
searchPaths.map(searchPath => {
// 1 open folder => just resolve the search paths to absolute paths
let { pathPortion, globPortion } = splitGlobFromPath(searchPath);
if (globPortion) {
globPortion = normalizeGlobPattern(globPortion);
}
// One pathPortion to multiple expanded search paths (eg duplicate matching workspace folders)
const oneExpanded = this.expandOneSearchPath(pathPortion);
// Expanded search paths to multiple resolved patterns (with ** and without)
return arrays.flatten(
oneExpanded.map(oneExpandedResult => this.resolveOneSearchPathPattern(oneExpandedResult, globPortion)));
}));
const searchPathPatternMap = new Map<string, ISearchPathPattern>();
expandedSearchPaths.forEach(oneSearchPathPattern => {
const key = oneSearchPathPattern.searchPath.toString();
const existing = searchPathPatternMap.get(key);
if (existing) {
if (oneSearchPathPattern.pattern) {
existing.pattern = existing.pattern || {};
existing.pattern[oneSearchPathPattern.pattern] = true;
}
} else {
searchPathPatternMap.set(key, {
searchPath: oneSearchPathPattern.searchPath,
pattern: oneSearchPathPattern.pattern ? patternListToIExpression(oneSearchPathPattern.pattern) : undefined
});
}
});
return values(searchPathPatternMap);
}
/**
* Takes a searchPath like `./a/foo` or `../a/foo` and expands it to absolute paths for all the workspaces it matches.
*/
private expandOneSearchPath(searchPath: string): IOneSearchPathPattern[] {
if (path.isAbsolute(searchPath)) {
// Currently only local resources can be searched for with absolute search paths.
// TODO convert this to a workspace folder + pattern, so excludes will be resolved properly for an absolute path inside a workspace folder
return [{
searchPath: uri.file(path.normalize(searchPath))
}];
}
if (this.workspaceContextService.getWorkbenchState() === WorkbenchState.FOLDER) {
const workspaceUri = this.workspaceContextService.getWorkspace().folders[0].uri;
searchPath = normalizeSlashes(searchPath);
if (strings.startsWith(searchPath, '../')) {
const resolvedPath = path.posix.resolve(workspaceUri.path, searchPath);
return [{
searchPath: workspaceUri.with({ path: resolvedPath })
}];
}
const cleanedPattern = normalizeGlobPattern(searchPath);
return [{
searchPath: workspaceUri,
pattern: cleanedPattern
}];
} else if (searchPath === './' || searchPath === '.\\') {
return []; // ./ or ./**/foo makes sense for single-folder but not multi-folder workspaces
} else {
const relativeSearchPathMatch = searchPath.match(/\.[\/\\]([^\/\\]+)(?:[\/\\](.+))?/);
if (relativeSearchPathMatch) {
const searchPathRoot = relativeSearchPathMatch[1];
const matchingRoots = this.workspaceContextService.getWorkspace().folders.filter(folder => folder.name === searchPathRoot);
if (matchingRoots.length) {
return matchingRoots.map(root => {
const patternMatch = relativeSearchPathMatch[2];
return {
searchPath: root.uri,
pattern: patternMatch && normalizeGlobPattern(patternMatch)
};
});
} else {
// No root folder with name
const searchPathNotFoundError = nls.localize('search.noWorkspaceWithName', "No folder in workspace with name: {0}", searchPathRoot);
throw new Error(searchPathNotFoundError);
}
} else {
// Malformed ./ search path, ignore
}
}
return [];
}
private resolveOneSearchPathPattern(oneExpandedResult: IOneSearchPathPattern, globPortion?: string): IOneSearchPathPattern[] {
const pattern = oneExpandedResult.pattern && globPortion ?
`${oneExpandedResult.pattern}/${globPortion}` :
oneExpandedResult.pattern || globPortion;
const results = [
{
searchPath: oneExpandedResult.searchPath,
pattern
}];
if (pattern && !strings.endsWith(pattern, '**')) {
results.push({
searchPath: oneExpandedResult.searchPath,
pattern: pattern + '/**'
});
}
return results;
}
private getFolderQueryForSearchPath(searchPath: ISearchPathPattern, options: ICommonQueryBuilderOptions, searchPathExcludes: ISearchPathsInfo): IFolderQuery | null {
const rootConfig = this.getFolderQueryForRoot(searchPath.searchPath, options, searchPathExcludes);
if (!rootConfig) {
return null;
}
return {
...rootConfig,
...{
includePattern: searchPath.pattern
}
};
}
private getFolderQueryForRoot(folder: uri, options: ICommonQueryBuilderOptions, searchPathExcludes: ISearchPathsInfo): IFolderQuery | null {
let thisFolderExcludeSearchPathPattern: glob.IExpression | undefined;
if (searchPathExcludes.searchPaths) {
const thisFolderExcludeSearchPath = searchPathExcludes.searchPaths.filter(sp => isEqual(sp.searchPath, folder))[0];
if (thisFolderExcludeSearchPath && !thisFolderExcludeSearchPath.pattern) {
// entire folder is excluded
return null;
} else if (thisFolderExcludeSearchPath) {
thisFolderExcludeSearchPathPattern = thisFolderExcludeSearchPath.pattern;
}
}
const folderConfig = this.configurationService.getValue<ISearchConfiguration>({ resource: folder });
const settingExcludes = this.getExcludesForFolder(folderConfig, options);
const excludePattern: glob.IExpression = {
...(settingExcludes || {}),
...(thisFolderExcludeSearchPathPattern || {})
};
return <IFolderQuery>{
folder,
excludePattern: Object.keys(excludePattern).length > 0 ? excludePattern : undefined,
fileEncoding: folderConfig.files && folderConfig.files.encoding,
disregardIgnoreFiles: typeof options.disregardIgnoreFiles === 'boolean' ? options.disregardIgnoreFiles : !folderConfig.search.useIgnoreFiles,
disregardGlobalIgnoreFiles: typeof options.disregardGlobalIgnoreFiles === 'boolean' ? options.disregardGlobalIgnoreFiles : !folderConfig.search.useGlobalIgnoreFiles,
ignoreSymlinks: typeof options.ignoreSymlinks === 'boolean' ? options.ignoreSymlinks : !folderConfig.search.followSymlinks,
};
}
}
function splitGlobFromPath(searchPath: string): { pathPortion: string, globPortion?: string } {
const globCharMatch = searchPath.match(/[\*\{\}\(\)\[\]\?]/);
if (globCharMatch) {
const globCharIdx = globCharMatch.index;
const lastSlashMatch = searchPath.substr(0, globCharIdx).match(/[/|\\][^/\\]*$/);
if (lastSlashMatch) {
let pathPortion = searchPath.substr(0, lastSlashMatch.index);
if (!pathPortion.match(/[/\\]/)) {
// If the last slash was the only slash, then we now have '' or 'C:' or '.'. Append a slash.
pathPortion += '/';
}
return {
pathPortion,
globPortion: searchPath.substr((lastSlashMatch.index || 0) + 1)
};
}
}
// No glob char, or malformed
return {
pathPortion: searchPath
};
}
function patternListToIExpression(...patterns: string[]): glob.IExpression {
return patterns.length ?
patterns.reduce((glob, cur) => { glob[cur] = true; return glob; }, Object.create(null)) :
undefined;
}
function splitGlobPattern(pattern: string): string[] {
return glob.splitGlobAware(pattern, ',')
.map(s => s.trim())
.filter(s => !!s.length);
}
/**
* Note - we used {} here previously but ripgrep can't handle nested {} patterns. See https://github.com/Microsoft/vscode/issues/32761
*/
function expandGlobalGlob(pattern: string): string[] {
const patterns = [
`**/${pattern}/**`,
`**/${pattern}`
];
return patterns.map(p => p.replace(/\*\*\/\*\*/g, '**'));
}
function normalizeSlashes(pattern: string): string {
return pattern.replace(/\\/g, '/');
}
/**
* Normalize slashes, remove `./` and trailing slashes
*/
function normalizeGlobPattern(pattern: string): string {
return normalizeSlashes(pattern)
.replace(/^\.\//, '')
.replace(/\/+$/g, '');
}

View File

@@ -0,0 +1,37 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Match, FileMatch, FileMatchOrMatch } from 'vs/workbench/contrib/search/common/searchModel';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IProgressRunner } from 'vs/platform/progress/common/progress';
export const IReplaceService = createDecorator<IReplaceService>('replaceService');
export interface IReplaceService {
_serviceBrand: any;
/**
* Replaces the given match in the file that match belongs to
*/
replace(match: Match): Promise<any>;
/**
* Replace all the matches from the given file matches in the files
* You can also pass the progress runner to update the progress of replacing.
*/
replace(files: FileMatch[], progress?: IProgressRunner): Promise<any>;
/**
* Opens the replace preview for given file match or match
*/
openReplacePreview(element: FileMatchOrMatch, preserveFocus?: boolean, sideBySide?: boolean, pinned?: boolean): Promise<any>;
/**
* Update the replace preview for the given file.
* If `override` is `true`, then replace preview is constructed from source model
*/
updateReplacePreview(file: FileMatch, override?: boolean): Promise<void>;
}

View File

@@ -0,0 +1,95 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { onUnexpectedError } from 'vs/base/common/errors';
import { IDisposable } from 'vs/base/common/lifecycle';
import { ISearchConfiguration, ISearchConfigurationProperties } from 'vs/workbench/services/search/common/search';
import { SymbolKind, Location, ProviderResult } from 'vs/editor/common/modes';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { URI } from 'vs/base/common/uri';
import { toResource } from 'vs/workbench/common/editor';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { CancellationToken } from 'vs/base/common/cancellation';
export interface IWorkspaceSymbol {
name: string;
containerName?: string;
kind: SymbolKind;
location: Location;
}
export interface IWorkspaceSymbolProvider {
provideWorkspaceSymbols(search: string, token: CancellationToken): ProviderResult<IWorkspaceSymbol[]>;
resolveWorkspaceSymbol?(item: IWorkspaceSymbol, token: CancellationToken): ProviderResult<IWorkspaceSymbol>;
}
export namespace WorkspaceSymbolProviderRegistry {
const _supports: IWorkspaceSymbolProvider[] = [];
export function register(provider: IWorkspaceSymbolProvider): IDisposable {
let support: IWorkspaceSymbolProvider | undefined = provider;
if (support) {
_supports.push(support);
}
return {
dispose() {
if (support) {
const idx = _supports.indexOf(support);
if (idx >= 0) {
_supports.splice(idx, 1);
support = undefined;
}
}
}
};
}
export function all(): IWorkspaceSymbolProvider[] {
return _supports.slice(0);
}
}
export function getWorkspaceSymbols(query: string, token: CancellationToken = CancellationToken.None): Promise<[IWorkspaceSymbolProvider, IWorkspaceSymbol[]][]> {
const result: [IWorkspaceSymbolProvider, IWorkspaceSymbol[]][] = [];
const promises = WorkspaceSymbolProviderRegistry.all().map(support => {
return Promise.resolve(support.provideWorkspaceSymbols(query, token)).then(value => {
if (Array.isArray(value)) {
result.push([support, value]);
}
}, onUnexpectedError);
});
return Promise.all(promises).then(_ => result);
}
export interface IWorkbenchSearchConfigurationProperties extends ISearchConfigurationProperties {
quickOpen: {
includeSymbols: boolean;
};
}
export interface IWorkbenchSearchConfiguration extends ISearchConfiguration {
search: IWorkbenchSearchConfigurationProperties;
}
/**
* Helper to return all opened editors with resources not belonging to the currently opened workspace.
*/
export function getOutOfWorkspaceEditorResources(editorService: IEditorService, contextService: IWorkspaceContextService): URI[] {
const resources: URI[] = [];
editorService.editors.forEach(editor => {
const resource = toResource(editor, { supportSideBySide: true });
if (resource && !contextService.isInsideWorkspace(resource)) {
resources.push(resource);
}
});
return resources;
}

View File

@@ -0,0 +1,67 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Emitter, Event } from 'vs/base/common/event';
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
import { isEmptyObject } from 'vs/base/common/types';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
export interface ISearchHistoryService {
_serviceBrand: any;
onDidClearHistory: Event<void>;
clearHistory(): void;
load(): ISearchHistoryValues;
save(history: ISearchHistoryValues): void;
}
export const ISearchHistoryService = createDecorator<ISearchHistoryService>('searchHistoryService');
export interface ISearchHistoryValues {
search?: string[];
replace?: string[];
include?: string[];
exclude?: string[];
}
export class SearchHistoryService implements ISearchHistoryService {
_serviceBrand: any;
private static readonly SEARCH_HISTORY_KEY = 'workbench.search.history';
private readonly _onDidClearHistory = new Emitter<void>();
readonly onDidClearHistory: Event<void> = this._onDidClearHistory.event;
constructor(
@IStorageService private readonly storageService: IStorageService
) { }
clearHistory(): void {
this.storageService.remove(SearchHistoryService.SEARCH_HISTORY_KEY, StorageScope.WORKSPACE);
this._onDidClearHistory.fire();
}
load(): ISearchHistoryValues {
let result: ISearchHistoryValues | undefined;
const raw = this.storageService.get(SearchHistoryService.SEARCH_HISTORY_KEY, StorageScope.WORKSPACE);
if (raw) {
try {
result = JSON.parse(raw);
} catch (e) {
// Invalid data
}
}
return result || {};
}
save(history: ISearchHistoryValues): void {
if (isEmptyObject(history)) {
this.storageService.remove(SearchHistoryService.SEARCH_HISTORY_KEY, StorageScope.WORKSPACE);
} else {
this.storageService.store(SearchHistoryService.SEARCH_HISTORY_KEY, JSON.stringify(history), StorageScope.WORKSPACE);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,88 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ITreeNavigator } from 'vs/base/browser/ui/tree/tree';
import { Emitter } from 'vs/base/common/event';
import { IDisposable } from 'vs/base/common/lifecycle';
const someEvent = new Emitter().event;
/**
* Add stub methods as needed
*/
export class MockObjectTree<T, TRef> implements IDisposable {
get onDidChangeFocus() { return someEvent; }
get onDidChangeSelection() { return someEvent; }
get onDidOpen() { return someEvent; }
get onMouseClick() { return someEvent; }
get onMouseDblClick() { return someEvent; }
get onContextMenu() { return someEvent; }
get onKeyDown() { return someEvent; }
get onKeyUp() { return someEvent; }
get onKeyPress() { return someEvent; }
get onDidFocus() { return someEvent; }
get onDidBlur() { return someEvent; }
get onDidChangeCollapseState() { return someEvent; }
get onDidChangeRenderNodeCount() { return someEvent; }
get onDidDispose() { return someEvent; }
constructor(private elements: any[]) { }
domFocus(): void { }
collapse(location: TRef, recursive: boolean = false): boolean {
return true;
}
expand(location: TRef, recursive: boolean = false): boolean {
return true;
}
navigate(start?: TRef): ITreeNavigator<T> {
const startIdx = start ? this.elements.indexOf(start) :
undefined;
return new ArrayNavigator(this.elements, startIdx);
}
dispose(): void {
}
}
class ArrayNavigator<T> implements ITreeNavigator<T> {
constructor(private elements: T[], private index = 0) { }
current(): T | null {
return this.elements[this.index];
}
previous(): T | null {
return this.elements[--this.index];
}
parent(): T | null {
throw new Error('not implemented');
}
first(): T | null {
this.index = 0;
return this.elements[this.index];
}
last(): T | null {
this.index = this.elements.length - 1;
return this.elements[this.index];
}
next(): T | null {
return this.elements[++this.index];
}
}

View File

@@ -0,0 +1,219 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import * as errors from 'vs/base/common/errors';
import * as objects from 'vs/base/common/objects';
import { CacheState } from 'vs/workbench/contrib/search/browser/openFileHandler';
import { DeferredPromise } from 'vs/base/test/common/utils';
import { QueryType, IFileQuery } from 'vs/workbench/services/search/common/search';
suite('CacheState', () => {
test('reuse old cacheKey until new cache is loaded', async function () {
const cache = new MockCache();
const first = createCacheState(cache);
const firstKey = first.cacheKey;
assert.strictEqual(first.isLoaded, false);
assert.strictEqual(first.isUpdating, false);
first.load();
assert.strictEqual(first.isLoaded, false);
assert.strictEqual(first.isUpdating, true);
await cache.loading[firstKey].complete(null);
assert.strictEqual(first.isLoaded, true);
assert.strictEqual(first.isUpdating, false);
const second = createCacheState(cache, first);
second.load();
assert.strictEqual(second.isLoaded, true);
assert.strictEqual(second.isUpdating, true);
await cache.awaitDisposal(0);
assert.strictEqual(second.cacheKey, firstKey); // still using old cacheKey
const secondKey = cache.cacheKeys[1];
await cache.loading[secondKey].complete(null);
assert.strictEqual(second.isLoaded, true);
assert.strictEqual(second.isUpdating, false);
await cache.awaitDisposal(1);
assert.strictEqual(second.cacheKey, secondKey);
});
test('do not spawn additional load if previous is still loading', async function () {
const cache = new MockCache();
const first = createCacheState(cache);
const firstKey = first.cacheKey;
first.load();
assert.strictEqual(first.isLoaded, false);
assert.strictEqual(first.isUpdating, true);
assert.strictEqual(Object.keys(cache.loading).length, 1);
const second = createCacheState(cache, first);
second.load();
assert.strictEqual(second.isLoaded, false);
assert.strictEqual(second.isUpdating, true);
assert.strictEqual(cache.cacheKeys.length, 2);
assert.strictEqual(Object.keys(cache.loading).length, 1); // still only one loading
assert.strictEqual(second.cacheKey, firstKey);
await cache.loading[firstKey].complete(null);
assert.strictEqual(second.isLoaded, true);
assert.strictEqual(second.isUpdating, false);
await cache.awaitDisposal(0);
});
test('do not use previous cacheKey if query changed', async function () {
const cache = new MockCache();
const first = createCacheState(cache);
const firstKey = first.cacheKey;
first.load();
await cache.loading[firstKey].complete(null);
assert.strictEqual(first.isLoaded, true);
assert.strictEqual(first.isUpdating, false);
await cache.awaitDisposal(0);
cache.baseQuery.excludePattern = { '**/node_modules': true };
const second = createCacheState(cache, first);
assert.strictEqual(second.isLoaded, false);
assert.strictEqual(second.isUpdating, false);
await cache.awaitDisposal(1);
second.load();
assert.strictEqual(second.isLoaded, false);
assert.strictEqual(second.isUpdating, true);
assert.notStrictEqual(second.cacheKey, firstKey); // not using old cacheKey
const secondKey = cache.cacheKeys[1];
assert.strictEqual(second.cacheKey, secondKey);
await cache.loading[secondKey].complete(null);
assert.strictEqual(second.isLoaded, true);
assert.strictEqual(second.isUpdating, false);
await cache.awaitDisposal(1);
});
test('dispose propagates', async function () {
const cache = new MockCache();
const first = createCacheState(cache);
const firstKey = first.cacheKey;
first.load();
await cache.loading[firstKey].complete(null);
const second = createCacheState(cache, first);
assert.strictEqual(second.isLoaded, true);
assert.strictEqual(second.isUpdating, false);
await cache.awaitDisposal(0);
second.dispose();
assert.strictEqual(second.isLoaded, false);
assert.strictEqual(second.isUpdating, false);
await cache.awaitDisposal(1);
assert.ok(cache.disposing[firstKey]);
});
test('keep using old cacheKey when loading fails', async function () {
const cache = new MockCache();
const first = createCacheState(cache);
const firstKey = first.cacheKey;
first.load();
await cache.loading[firstKey].complete(null);
const second = createCacheState(cache, first);
second.load();
const secondKey = cache.cacheKeys[1];
const origErrorHandler = errors.errorHandler.getUnexpectedErrorHandler();
try {
errors.setUnexpectedErrorHandler(() => null);
await cache.loading[secondKey].error('loading failed');
} finally {
errors.setUnexpectedErrorHandler(origErrorHandler);
}
assert.strictEqual(second.isLoaded, true);
assert.strictEqual(second.isUpdating, false);
assert.strictEqual(Object.keys(cache.loading).length, 2);
await cache.awaitDisposal(0);
assert.strictEqual(second.cacheKey, firstKey); // keep using old cacheKey
const third = createCacheState(cache, second);
third.load();
assert.strictEqual(third.isLoaded, true);
assert.strictEqual(third.isUpdating, true);
assert.strictEqual(Object.keys(cache.loading).length, 3);
await cache.awaitDisposal(0);
assert.strictEqual(third.cacheKey, firstKey);
const thirdKey = cache.cacheKeys[2];
await cache.loading[thirdKey].complete(null);
assert.strictEqual(third.isLoaded, true);
assert.strictEqual(third.isUpdating, false);
assert.strictEqual(Object.keys(cache.loading).length, 3);
await cache.awaitDisposal(2);
assert.strictEqual(third.cacheKey, thirdKey); // recover with next successful load
});
function createCacheState(cache: MockCache, previous?: CacheState): CacheState {
return new CacheState(
cacheKey => cache.query(cacheKey),
query => cache.load(query),
cacheKey => cache.dispose(cacheKey),
previous!
);
}
class MockCache {
public cacheKeys: string[] = [];
public loading: { [cacheKey: string]: DeferredPromise<any> } = {};
public disposing: { [cacheKey: string]: DeferredPromise<void> } = {};
private _awaitDisposal: (() => void)[][] = [];
public baseQuery: IFileQuery = {
type: QueryType.File,
folderQueries: []
};
public query(cacheKey: string): IFileQuery {
this.cacheKeys.push(cacheKey);
return objects.assign({ cacheKey: cacheKey }, this.baseQuery);
}
public load(query: IFileQuery): Promise<any> {
const promise = new DeferredPromise<any>();
this.loading[query.cacheKey!] = promise;
return promise.p;
}
public dispose(cacheKey: string): Promise<void> {
const promise = new DeferredPromise<void>();
this.disposing[cacheKey] = promise;
const n = Object.keys(this.disposing).length;
for (const done of this._awaitDisposal[n] || []) {
done();
}
delete this._awaitDisposal[n];
return promise.p;
}
public awaitDisposal(n: number) {
return new Promise(resolve => {
if (n === Object.keys(this.disposing).length) {
resolve();
} else {
(this._awaitDisposal[n] || (this._awaitDisposal[n] = [])).push(resolve);
}
});
}
}
});

View File

@@ -0,0 +1,158 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { Keybinding } from 'vs/base/common/keyCodes';
import { OS } from 'vs/base/common/platform';
import { URI } from 'vs/base/common/uri';
import { IModelService } from 'vs/editor/common/services/modelService';
import { ModelServiceImpl } from 'vs/editor/common/services/modelServiceImpl';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService';
import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { USLayoutResolvedKeybinding } from 'vs/platform/keybinding/common/usLayoutResolvedKeybinding';
import { IFileMatch } from 'vs/workbench/services/search/common/search';
import { ReplaceAction } from 'vs/workbench/contrib/search/browser/searchActions';
import { FileMatch, FileMatchOrMatch, Match } from 'vs/workbench/contrib/search/common/searchModel';
import { MockObjectTree } from 'vs/workbench/contrib/search/test/browser/mockSearchTree';
suite('Search Actions', () => {
let instantiationService: TestInstantiationService;
let counter: number;
setup(() => {
instantiationService = new TestInstantiationService();
instantiationService.stub(IModelService, stubModelService(instantiationService));
instantiationService.stub(IKeybindingService, {});
instantiationService.stub(IKeybindingService, 'resolveKeybinding', (keybinding: Keybinding) => [new USLayoutResolvedKeybinding(keybinding, OS)]);
instantiationService.stub(IKeybindingService, 'lookupKeybinding', (id: string) => null);
counter = 0;
});
test('get next element to focus after removing a match when it has next sibling file', function () {
const fileMatch1 = aFileMatch();
const fileMatch2 = aFileMatch();
const data = [fileMatch1, aMatch(fileMatch1), aMatch(fileMatch1), fileMatch2, aMatch(fileMatch2), aMatch(fileMatch2)];
const tree = aTree(data);
const target = data[2];
const testObject: ReplaceAction = instantiationService.createInstance(ReplaceAction, tree, target, null);
const actual = testObject.getElementToFocusAfterRemoved(tree, target);
assert.equal(data[4], actual);
});
test('get next element to focus after removing a match when it does not have next sibling match', function () {
const fileMatch1 = aFileMatch();
const fileMatch2 = aFileMatch();
const data = [fileMatch1, aMatch(fileMatch1), aMatch(fileMatch1), fileMatch2, aMatch(fileMatch2), aMatch(fileMatch2)];
const tree = aTree(data);
const target = data[5];
const testObject: ReplaceAction = instantiationService.createInstance(ReplaceAction, tree, target, null);
const actual = testObject.getElementToFocusAfterRemoved(tree, target);
assert.equal(data[4], actual);
});
test('get next element to focus after removing a match when it does not have next sibling match and previous match is file match', function () {
const fileMatch1 = aFileMatch();
const fileMatch2 = aFileMatch();
const data = [fileMatch1, aMatch(fileMatch1), aMatch(fileMatch1), fileMatch2, aMatch(fileMatch2)];
const tree = aTree(data);
const target = data[4];
const testObject: ReplaceAction = instantiationService.createInstance(ReplaceAction, tree, target, null);
const actual = testObject.getElementToFocusAfterRemoved(tree, target);
assert.equal(data[2], actual);
});
test('get next element to focus after removing a match when it is the only match', function () {
const fileMatch1 = aFileMatch();
const data = [fileMatch1, aMatch(fileMatch1)];
const tree = aTree(data);
const target = data[1];
const testObject: ReplaceAction = instantiationService.createInstance(ReplaceAction, tree, target, null);
const actual = testObject.getElementToFocusAfterRemoved(tree, target);
assert.equal(undefined, actual);
});
test('get next element to focus after removing a file match when it has next sibling', function () {
const fileMatch1 = aFileMatch();
const fileMatch2 = aFileMatch();
const fileMatch3 = aFileMatch();
const data = [fileMatch1, aMatch(fileMatch1), fileMatch2, aMatch(fileMatch2), fileMatch3, aMatch(fileMatch3)];
const tree = aTree(data);
const target = data[2];
const testObject: ReplaceAction = instantiationService.createInstance(ReplaceAction, tree, target, null);
const actual = testObject.getElementToFocusAfterRemoved(tree, target);
assert.equal(data[4], actual);
});
test('get next element to focus after removing a file match when it has no next sibling', function () {
const fileMatch1 = aFileMatch();
const fileMatch2 = aFileMatch();
const fileMatch3 = aFileMatch();
const data = [fileMatch1, aMatch(fileMatch1), fileMatch2, aMatch(fileMatch2), fileMatch3, aMatch(fileMatch3)];
const tree = aTree(data);
const target = data[4];
const testObject: ReplaceAction = instantiationService.createInstance(ReplaceAction, tree, target, null);
const actual = testObject.getElementToFocusAfterRemoved(tree, target);
assert.equal(data[3], actual);
});
test('get next element to focus after removing a file match when it is only match', function () {
const fileMatch1 = aFileMatch();
const data = [fileMatch1, aMatch(fileMatch1)];
const tree = aTree(data);
const target = data[0];
const testObject: ReplaceAction = instantiationService.createInstance(ReplaceAction, tree, target, null);
const actual = testObject.getElementToFocusAfterRemoved(tree, target);
assert.equal(undefined, actual);
});
function aFileMatch(): FileMatch {
const rawMatch: IFileMatch = {
resource: URI.file('somepath' + ++counter),
results: []
};
return instantiationService.createInstance(FileMatch, null, null, null, null, rawMatch);
}
function aMatch(fileMatch: FileMatch): Match {
const line = ++counter;
const match = new Match(
fileMatch,
['some match'],
{
startLineNumber: 0,
startColumn: 0,
endLineNumber: 0,
endColumn: 2
},
{
startLineNumber: line,
startColumn: 0,
endLineNumber: line,
endColumn: 2
}
);
fileMatch.add(match);
return match;
}
function aTree(elements: FileMatchOrMatch[]): any {
return new MockObjectTree(elements);
}
function stubModelService(instantiationService: TestInstantiationService): IModelService {
instantiationService.stub(IConfigurationService, new TestConfigurationService());
return instantiationService.createInstance(ModelServiceImpl);
}
});

View File

@@ -0,0 +1,95 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { URI as uri } from 'vs/base/common/uri';
import { IModelService } from 'vs/editor/common/services/modelService';
import { ModelServiceImpl } from 'vs/editor/common/services/modelServiceImpl';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService';
import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock';
import { IFileMatch, ITextSearchMatch, OneLineRange, QueryType } from 'vs/workbench/services/search/common/search';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { TestWorkspace } from 'vs/platform/workspace/test/common/testWorkspace';
import { FileMatch, Match, searchMatchComparer, SearchResult } from 'vs/workbench/contrib/search/common/searchModel';
import { TestContextService } from 'vs/workbench/test/workbenchTestServices';
suite('Search - Viewlet', () => {
let instantiation: TestInstantiationService;
setup(() => {
instantiation = new TestInstantiationService();
instantiation.stub(IModelService, stubModelService(instantiation));
instantiation.set(IWorkspaceContextService, new TestContextService(TestWorkspace));
});
test('Data Source', function () {
let result: SearchResult = instantiation.createInstance(SearchResult, null);
result.query = {
type: QueryType.Text,
contentPattern: { pattern: 'foo' },
folderQueries: [{
folder: uri.parse('file://c:/')
}]
};
result.add([{
resource: uri.parse('file:///c:/foo'),
results: [{
preview: {
text: 'bar',
matches: {
startLineNumber: 0,
startColumn: 0,
endLineNumber: 0,
endColumn: 1
}
},
ranges: {
startLineNumber: 1,
startColumn: 0,
endLineNumber: 1,
endColumn: 1
}
}]
}]);
let fileMatch = result.matches()[0];
let lineMatch = fileMatch.matches()[0];
assert.equal(fileMatch.id(), 'file:///c%3A/foo');
assert.equal(lineMatch.id(), 'file:///c%3A/foo>[2,1 -> 2,2]b');
});
test('Comparer', () => {
let fileMatch1 = aFileMatch('C:\\foo');
let fileMatch2 = aFileMatch('C:\\with\\path');
let fileMatch3 = aFileMatch('C:\\with\\path\\foo');
let lineMatch1 = new Match(fileMatch1, ['bar'], new OneLineRange(0, 1, 1), new OneLineRange(0, 1, 1));
let lineMatch2 = new Match(fileMatch1, ['bar'], new OneLineRange(0, 1, 1), new OneLineRange(2, 1, 1));
let lineMatch3 = new Match(fileMatch1, ['bar'], new OneLineRange(0, 1, 1), new OneLineRange(2, 1, 1));
assert(searchMatchComparer(fileMatch1, fileMatch2) < 0);
assert(searchMatchComparer(fileMatch2, fileMatch1) > 0);
assert(searchMatchComparer(fileMatch1, fileMatch1) === 0);
assert(searchMatchComparer(fileMatch2, fileMatch3) < 0);
assert(searchMatchComparer(lineMatch1, lineMatch2) < 0);
assert(searchMatchComparer(lineMatch2, lineMatch1) > 0);
assert(searchMatchComparer(lineMatch2, lineMatch3) === 0);
});
function aFileMatch(path: string, searchResult?: SearchResult, ...lineMatches: ITextSearchMatch[]): FileMatch {
let rawMatch: IFileMatch = {
resource: uri.file('C:\\' + path),
results: lineMatches
};
return instantiation.createInstance(FileMatch, null, null, null, searchResult, rawMatch);
}
function stubModelService(instantiationService: TestInstantiationService): IModelService {
instantiationService.stub(IConfigurationService, new TestConfigurationService());
return instantiationService.createInstance(ModelServiceImpl);
}
});

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,329 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import * as sinon from 'sinon';
import { timeout } from 'vs/base/common/async';
import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation';
import { URI } from 'vs/base/common/uri';
import { DeferredPromise } from 'vs/base/test/common/utils';
import { Range } from 'vs/editor/common/core/range';
import { IModelService } from 'vs/editor/common/services/modelService';
import { ModelServiceImpl } from 'vs/editor/common/services/modelServiceImpl';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService';
import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock';
import { IFileMatch, IFileSearchStats, IFolderQuery, ISearchComplete, ISearchProgressItem, ISearchQuery, ISearchService, ITextSearchMatch, OneLineRange, TextSearchMatch } from 'vs/workbench/services/search/common/search';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils';
import { SearchModel } from 'vs/workbench/contrib/search/common/searchModel';
const nullEvent = new class {
id: number;
topic: string;
name: string;
description: string;
data: any;
startTime: Date;
stopTime: Date;
stop(): void {
return;
}
timeTaken(): number {
return -1;
}
};
const lineOneRange = new OneLineRange(1, 0, 1);
suite('SearchModel', () => {
let instantiationService: TestInstantiationService;
let restoreStubs: sinon.SinonStub[];
const testSearchStats: IFileSearchStats = {
fromCache: false,
resultCount: 1,
type: 'searchProcess',
detailStats: {
fileWalkTime: 0,
cmdTime: 0,
cmdResultCount: 0,
directoriesWalked: 2,
filesWalked: 3
}
};
const folderQueries: IFolderQuery[] = [
{ folder: URI.parse('file://c:/') }
];
setup(() => {
restoreStubs = [];
instantiationService = new TestInstantiationService();
instantiationService.stub(ITelemetryService, NullTelemetryService);
instantiationService.stub(IModelService, stubModelService(instantiationService));
instantiationService.stub(ISearchService, {});
instantiationService.stub(ISearchService, 'textSearch', Promise.resolve({ results: [] }));
});
teardown(() => {
restoreStubs.forEach(element => {
element.restore();
});
});
function searchServiceWithResults(results: IFileMatch[], complete: ISearchComplete | null = null): ISearchService {
return <ISearchService>{
textSearch(query: ISearchQuery, token?: CancellationToken, onProgress?: (result: ISearchProgressItem) => void): Promise<ISearchComplete> {
return new Promise(resolve => {
process.nextTick(() => {
results.forEach(onProgress!);
resolve(complete!);
});
});
}
};
}
function searchServiceWithError(error: Error): ISearchService {
return <ISearchService>{
textSearch(query: ISearchQuery, token?: CancellationToken, onProgress?: (result: ISearchProgressItem) => void): Promise<ISearchComplete> {
return new Promise((resolve, reject) => {
reject(error);
});
}
};
}
function canceleableSearchService(tokenSource: CancellationTokenSource): ISearchService {
return <ISearchService>{
textSearch(query: ISearchQuery, token?: CancellationToken, onProgress?: (result: ISearchProgressItem) => void): Promise<ISearchComplete> {
if (token) {
token.onCancellationRequested(() => tokenSource.cancel());
}
return new Promise(resolve => {
process.nextTick(() => {
resolve(<any>{});
});
});
}
};
}
test('Search Model: Search adds to results', async () => {
const results = [
aRawMatch('file://c:/1',
new TextSearchMatch('preview 1', new OneLineRange(1, 1, 4)),
new TextSearchMatch('preview 1', new OneLineRange(1, 4, 11))),
aRawMatch('file://c:/2', new TextSearchMatch('preview 2', lineOneRange))];
instantiationService.stub(ISearchService, searchServiceWithResults(results));
const testObject: SearchModel = instantiationService.createInstance(SearchModel);
await testObject.search({ contentPattern: { pattern: 'somestring' }, type: 1, folderQueries });
const actual = testObject.searchResult.matches();
assert.equal(2, actual.length);
assert.equal('file://c:/1', actual[0].resource().toString());
let actuaMatches = actual[0].matches();
assert.equal(2, actuaMatches.length);
assert.equal('preview 1', actuaMatches[0].text());
assert.ok(new Range(2, 2, 2, 5).equalsRange(actuaMatches[0].range()));
assert.equal('preview 1', actuaMatches[1].text());
assert.ok(new Range(2, 5, 2, 12).equalsRange(actuaMatches[1].range()));
actuaMatches = actual[1].matches();
assert.equal(1, actuaMatches.length);
assert.equal('preview 2', actuaMatches[0].text());
assert.ok(new Range(2, 1, 2, 2).equalsRange(actuaMatches[0].range()));
});
test('Search Model: Search reports telemetry on search completed', async () => {
const target = instantiationService.spy(ITelemetryService, 'publicLog');
const results = [
aRawMatch('file://c:/1',
new TextSearchMatch('preview 1', new OneLineRange(1, 1, 4)),
new TextSearchMatch('preview 1', new OneLineRange(1, 4, 11))),
aRawMatch('file://c:/2',
new TextSearchMatch('preview 2', lineOneRange))];
instantiationService.stub(ISearchService, searchServiceWithResults(results));
const testObject: SearchModel = instantiationService.createInstance(SearchModel);
await testObject.search({ contentPattern: { pattern: 'somestring' }, type: 1, folderQueries });
assert.ok(target.calledThrice);
const data = target.args[0];
data[1].duration = -1;
assert.deepEqual(['searchResultsFirstRender', { duration: -1 }], data);
});
test('Search Model: Search reports timed telemetry on search when progress is not called', () => {
const target2 = sinon.spy();
stub(nullEvent, 'stop', target2);
const target1 = sinon.stub().returns(nullEvent);
instantiationService.stub(ITelemetryService, 'publicLog', target1);
instantiationService.stub(ISearchService, searchServiceWithResults([]));
const testObject = instantiationService.createInstance(SearchModel);
const result = testObject.search({ contentPattern: { pattern: 'somestring' }, type: 1, folderQueries });
return result.then(() => {
return timeout(1).then(() => {
assert.ok(target1.calledWith('searchResultsFirstRender'));
assert.ok(target1.calledWith('searchResultsFinished'));
});
});
});
test('Search Model: Search reports timed telemetry on search when progress is called', () => {
const target2 = sinon.spy();
stub(nullEvent, 'stop', target2);
const target1 = sinon.stub().returns(nullEvent);
instantiationService.stub(ITelemetryService, 'publicLog', target1);
instantiationService.stub(ISearchService, searchServiceWithResults(
[aRawMatch('file://c:/1', new TextSearchMatch('some preview', lineOneRange))],
{ results: [], stats: testSearchStats }));
const testObject = instantiationService.createInstance(SearchModel);
const result = testObject.search({ contentPattern: { pattern: 'somestring' }, type: 1, folderQueries });
return result.then(() => {
return timeout(1).then(() => {
// timeout because promise handlers may run in a different order. We only care that these
// are fired at some point.
assert.ok(target1.calledWith('searchResultsFirstRender'));
assert.ok(target1.calledWith('searchResultsFinished'));
// assert.equal(1, target2.callCount);
});
});
});
test('Search Model: Search reports timed telemetry on search when error is called', () => {
const target2 = sinon.spy();
stub(nullEvent, 'stop', target2);
const target1 = sinon.stub().returns(nullEvent);
instantiationService.stub(ITelemetryService, 'publicLog', target1);
instantiationService.stub(ISearchService, searchServiceWithError(new Error('error')));
const testObject = instantiationService.createInstance(SearchModel);
const result = testObject.search({ contentPattern: { pattern: 'somestring' }, type: 1, folderQueries });
return result.then(() => { }, () => {
return timeout(1).then(() => {
assert.ok(target1.calledWith('searchResultsFirstRender'));
assert.ok(target1.calledWith('searchResultsFinished'));
// assert.ok(target2.calledOnce);
});
});
});
test('Search Model: Search reports timed telemetry on search when error is cancelled error', () => {
const target2 = sinon.spy();
stub(nullEvent, 'stop', target2);
const target1 = sinon.stub().returns(nullEvent);
instantiationService.stub(ITelemetryService, 'publicLog', target1);
const deferredPromise = new DeferredPromise<ISearchComplete>();
instantiationService.stub(ISearchService, 'textSearch', deferredPromise.p);
const testObject = instantiationService.createInstance(SearchModel);
const result = testObject.search({ contentPattern: { pattern: 'somestring' }, type: 1, folderQueries });
deferredPromise.cancel();
return result.then(() => { }, () => {
return timeout(1).then(() => {
assert.ok(target1.calledWith('searchResultsFirstRender'));
assert.ok(target1.calledWith('searchResultsFinished'));
// assert.ok(target2.calledOnce);
});
});
});
test('Search Model: Search results are cleared during search', async () => {
const results = [
aRawMatch('file://c:/1',
new TextSearchMatch('preview 1', new OneLineRange(1, 1, 4)),
new TextSearchMatch('preview 1', new OneLineRange(1, 4, 11))),
aRawMatch('file://c:/2',
new TextSearchMatch('preview 2', lineOneRange))];
instantiationService.stub(ISearchService, searchServiceWithResults(results));
const testObject: SearchModel = instantiationService.createInstance(SearchModel);
await testObject.search({ contentPattern: { pattern: 'somestring' }, type: 1, folderQueries });
assert.ok(!testObject.searchResult.isEmpty());
instantiationService.stub(ISearchService, searchServiceWithResults([]));
testObject.search({ contentPattern: { pattern: 'somestring' }, type: 1, folderQueries });
assert.ok(testObject.searchResult.isEmpty());
});
test('Search Model: Previous search is cancelled when new search is called', async () => {
const tokenSource = new CancellationTokenSource();
instantiationService.stub(ISearchService, canceleableSearchService(tokenSource));
const testObject: SearchModel = instantiationService.createInstance(SearchModel);
testObject.search({ contentPattern: { pattern: 'somestring' }, type: 1, folderQueries });
instantiationService.stub(ISearchService, searchServiceWithResults([]));
testObject.search({ contentPattern: { pattern: 'somestring' }, type: 1, folderQueries });
assert.ok(tokenSource.token.isCancellationRequested);
});
test('getReplaceString returns proper replace string for regExpressions', async () => {
const results = [
aRawMatch('file://c:/1',
new TextSearchMatch('preview 1', new OneLineRange(1, 1, 4)),
new TextSearchMatch('preview 1', new OneLineRange(1, 4, 11)))];
instantiationService.stub(ISearchService, searchServiceWithResults(results));
const testObject: SearchModel = instantiationService.createInstance(SearchModel);
await testObject.search({ contentPattern: { pattern: 're' }, type: 1, folderQueries });
testObject.replaceString = 'hello';
let match = testObject.searchResult.matches()[0].matches()[0];
assert.equal('hello', match.replaceString);
await testObject.search({ contentPattern: { pattern: 're', isRegExp: true }, type: 1, folderQueries });
match = testObject.searchResult.matches()[0].matches()[0];
assert.equal('hello', match.replaceString);
await testObject.search({ contentPattern: { pattern: 're(?:vi)', isRegExp: true }, type: 1, folderQueries });
match = testObject.searchResult.matches()[0].matches()[0];
assert.equal('hello', match.replaceString);
await testObject.search({ contentPattern: { pattern: 'r(e)(?:vi)', isRegExp: true }, type: 1, folderQueries });
match = testObject.searchResult.matches()[0].matches()[0];
assert.equal('hello', match.replaceString);
await testObject.search({ contentPattern: { pattern: 'r(e)(?:vi)', isRegExp: true }, type: 1, folderQueries });
testObject.replaceString = 'hello$1';
match = testObject.searchResult.matches()[0].matches()[0];
assert.equal('helloe', match.replaceString);
});
function aRawMatch(resource: string, ...results: ITextSearchMatch[]): IFileMatch {
return { resource: URI.parse(resource), results };
}
function stub(arg1: any, arg2: any, arg3: any): sinon.SinonStub {
const stub = sinon.stub(arg1, arg2, arg3);
restoreStubs.push(stub);
return stub;
}
function stubModelService(instantiationService: TestInstantiationService): IModelService {
instantiationService.stub(IConfigurationService, new TestConfigurationService());
return instantiationService.createInstance(ModelServiceImpl);
}
});

View File

@@ -0,0 +1,340 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import * as sinon from 'sinon';
import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock';
import { Match, FileMatch, SearchResult, SearchModel } from 'vs/workbench/contrib/search/common/searchModel';
import { URI } from 'vs/base/common/uri';
import { IFileMatch, TextSearchMatch, OneLineRange, ITextSearchMatch } from 'vs/workbench/services/search/common/search';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils';
import { Range } from 'vs/editor/common/core/range';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService';
import { ModelServiceImpl } from 'vs/editor/common/services/modelServiceImpl';
import { IModelService } from 'vs/editor/common/services/modelService';
import { IReplaceService } from 'vs/workbench/contrib/search/common/replace';
const lineOneRange = new OneLineRange(1, 0, 1);
suite('SearchResult', () => {
let instantiationService: TestInstantiationService;
setup(() => {
instantiationService = new TestInstantiationService();
instantiationService.stub(ITelemetryService, NullTelemetryService);
instantiationService.stub(IModelService, stubModelService(instantiationService));
instantiationService.stubPromise(IReplaceService, {});
instantiationService.stubPromise(IReplaceService, 'replace', null);
});
test('Line Match', function () {
const fileMatch = aFileMatch('folder/file.txt', null!);
const lineMatch = new Match(fileMatch, ['foo bar'], new OneLineRange(0, 0, 3), new OneLineRange(1, 0, 3));
assert.equal(lineMatch.text(), 'foo bar');
assert.equal(lineMatch.range().startLineNumber, 2);
assert.equal(lineMatch.range().endLineNumber, 2);
assert.equal(lineMatch.range().startColumn, 1);
assert.equal(lineMatch.range().endColumn, 4);
assert.equal('file:///folder/file.txt>[2,1 -> 2,4]foo', lineMatch.id());
assert.equal(lineMatch.fullMatchText(), 'foo');
assert.equal(lineMatch.fullMatchText(true), 'foo bar');
});
test('Line Match - Remove', function () {
const fileMatch = aFileMatch('folder/file.txt', aSearchResult(), new TextSearchMatch('foo bar', new OneLineRange(1, 0, 3)));
const lineMatch = fileMatch.matches()[0];
fileMatch.remove(lineMatch);
assert.equal(fileMatch.matches().length, 0);
});
test('File Match', function () {
let fileMatch = aFileMatch('folder/file.txt');
assert.equal(fileMatch.matches(), 0);
assert.equal(fileMatch.resource().toString(), 'file:///folder/file.txt');
assert.equal(fileMatch.name(), 'file.txt');
fileMatch = aFileMatch('file.txt');
assert.equal(fileMatch.matches(), 0);
assert.equal(fileMatch.resource().toString(), 'file:///file.txt');
assert.equal(fileMatch.name(), 'file.txt');
});
test('File Match: Select an existing match', function () {
const testObject = aFileMatch(
'folder/file.txt',
aSearchResult(),
new TextSearchMatch('foo', new OneLineRange(1, 0, 3)),
new TextSearchMatch('bar', new OneLineRange(1, 5, 3)));
testObject.setSelectedMatch(testObject.matches()[0]);
assert.equal(testObject.matches()[0], testObject.getSelectedMatch());
});
test('File Match: Select non existing match', function () {
const testObject = aFileMatch(
'folder/file.txt',
aSearchResult(),
new TextSearchMatch('foo', new OneLineRange(1, 0, 3)),
new TextSearchMatch('bar', new OneLineRange(1, 5, 3)));
const target = testObject.matches()[0];
testObject.remove(target);
testObject.setSelectedMatch(target);
assert.equal(undefined, testObject.getSelectedMatch());
});
test('File Match: isSelected return true for selected match', function () {
const testObject = aFileMatch(
'folder/file.txt',
aSearchResult(),
new TextSearchMatch('foo', new OneLineRange(1, 0, 3)),
new TextSearchMatch('bar', new OneLineRange(1, 5, 3)));
const target = testObject.matches()[0];
testObject.setSelectedMatch(target);
assert.ok(testObject.isMatchSelected(target));
});
test('File Match: isSelected return false for un-selected match', function () {
const testObject = aFileMatch('folder/file.txt',
aSearchResult(),
new TextSearchMatch('foo', new OneLineRange(1, 0, 3)),
new TextSearchMatch('bar', new OneLineRange(1, 5, 3)));
testObject.setSelectedMatch(testObject.matches()[0]);
assert.ok(!testObject.isMatchSelected(testObject.matches()[1]));
});
test('File Match: unselect', function () {
const testObject = aFileMatch(
'folder/file.txt',
aSearchResult(),
new TextSearchMatch('foo', new OneLineRange(1, 0, 3)),
new TextSearchMatch('bar', new OneLineRange(1, 5, 3)));
testObject.setSelectedMatch(testObject.matches()[0]);
testObject.setSelectedMatch(null);
assert.equal(null, testObject.getSelectedMatch());
});
test('File Match: unselect when not selected', function () {
const testObject = aFileMatch(
'folder/file.txt',
aSearchResult(),
new TextSearchMatch('foo', new OneLineRange(1, 0, 3)),
new TextSearchMatch('bar', new OneLineRange(1, 5, 3)));
testObject.setSelectedMatch(null);
assert.equal(null, testObject.getSelectedMatch());
});
test('Alle Drei Zusammen', function () {
const searchResult = instantiationService.createInstance(SearchResult, null);
const fileMatch = aFileMatch('far/boo', searchResult);
const lineMatch = new Match(fileMatch, ['foo bar'], new OneLineRange(0, 0, 3), new OneLineRange(1, 0, 3));
assert(lineMatch.parent() === fileMatch);
assert(fileMatch.parent() === searchResult);
});
test('Adding a raw match will add a file match with line matches', function () {
const testObject = aSearchResult();
const target = [aRawMatch('file://c:/',
new TextSearchMatch('preview 1', new OneLineRange(1, 1, 4)),
new TextSearchMatch('preview 1', new OneLineRange(1, 4, 11)),
new TextSearchMatch('preview 2', lineOneRange))];
testObject.add(target);
assert.equal(3, testObject.count());
const actual = testObject.matches();
assert.equal(1, actual.length);
assert.equal('file://c:/', actual[0].resource().toString());
const actuaMatches = actual[0].matches();
assert.equal(3, actuaMatches.length);
assert.equal('preview 1', actuaMatches[0].text());
assert.ok(new Range(2, 2, 2, 5).equalsRange(actuaMatches[0].range()));
assert.equal('preview 1', actuaMatches[1].text());
assert.ok(new Range(2, 5, 2, 12).equalsRange(actuaMatches[1].range()));
assert.equal('preview 2', actuaMatches[2].text());
assert.ok(new Range(2, 1, 2, 2).equalsRange(actuaMatches[2].range()));
});
test('Adding multiple raw matches', function () {
const testObject = aSearchResult();
const target = [
aRawMatch('file://c:/1',
new TextSearchMatch('preview 1', new OneLineRange(1, 1, 4)),
new TextSearchMatch('preview 1', new OneLineRange(1, 4, 11))),
aRawMatch('file://c:/2',
new TextSearchMatch('preview 2', lineOneRange))];
testObject.add(target);
assert.equal(3, testObject.count());
const actual = testObject.matches();
assert.equal(2, actual.length);
assert.equal('file://c:/1', actual[0].resource().toString());
let actuaMatches = actual[0].matches();
assert.equal(2, actuaMatches.length);
assert.equal('preview 1', actuaMatches[0].text());
assert.ok(new Range(2, 2, 2, 5).equalsRange(actuaMatches[0].range()));
assert.equal('preview 1', actuaMatches[1].text());
assert.ok(new Range(2, 5, 2, 12).equalsRange(actuaMatches[1].range()));
actuaMatches = actual[1].matches();
assert.equal(1, actuaMatches.length);
assert.equal('preview 2', actuaMatches[0].text());
assert.ok(new Range(2, 1, 2, 2).equalsRange(actuaMatches[0].range()));
});
test('Dispose disposes matches', function () {
const target1 = sinon.spy();
const target2 = sinon.spy();
const testObject = aSearchResult();
testObject.add([
aRawMatch('file://c:/1',
new TextSearchMatch('preview 1', lineOneRange)),
aRawMatch('file://c:/2',
new TextSearchMatch('preview 2', lineOneRange))]);
testObject.matches()[0].onDispose(target1);
testObject.matches()[1].onDispose(target2);
testObject.dispose();
assert.ok(testObject.isEmpty());
assert.ok(target1.calledOnce);
assert.ok(target2.calledOnce);
});
test('remove triggers change event', function () {
const target = sinon.spy();
const testObject = aSearchResult();
testObject.add([
aRawMatch('file://c:/1',
new TextSearchMatch('preview 1', lineOneRange))]);
const objectRoRemove = testObject.matches()[0];
testObject.onChange(target);
testObject.remove(objectRoRemove);
assert.ok(target.calledOnce);
assert.deepEqual([{ elements: [objectRoRemove], removed: true }], target.args[0]);
});
test('remove triggers change event', function () {
const target = sinon.spy();
const testObject = aSearchResult();
testObject.add([
aRawMatch('file://c:/1',
new TextSearchMatch('preview 1', lineOneRange))]);
const objectRoRemove = testObject.matches()[0];
testObject.onChange(target);
testObject.remove(objectRoRemove);
assert.ok(target.calledOnce);
assert.deepEqual([{ elements: [objectRoRemove], removed: true }], target.args[0]);
});
test('Removing all line matches and adding back will add file back to result', function () {
const testObject = aSearchResult();
testObject.add([
aRawMatch('file://c:/1',
new TextSearchMatch('preview 1', lineOneRange))]);
const target = testObject.matches()[0];
const matchToRemove = target.matches()[0];
target.remove(matchToRemove);
assert.ok(testObject.isEmpty());
target.add(matchToRemove, true);
assert.equal(1, testObject.fileCount());
assert.equal(target, testObject.matches()[0]);
});
test('replace should remove the file match', function () {
const voidPromise = Promise.resolve(null);
instantiationService.stub(IReplaceService, 'replace', voidPromise);
const testObject = aSearchResult();
testObject.add([
aRawMatch('file://c:/1',
new TextSearchMatch('preview 1', lineOneRange))]);
testObject.replace(testObject.matches()[0]);
return voidPromise.then(() => assert.ok(testObject.isEmpty()));
});
test('replace should trigger the change event', function () {
const target = sinon.spy();
const voidPromise = Promise.resolve(null);
instantiationService.stub(IReplaceService, 'replace', voidPromise);
const testObject = aSearchResult();
testObject.add([
aRawMatch('file://c:/1',
new TextSearchMatch('preview 1', lineOneRange))]);
testObject.onChange(target);
const objectRoRemove = testObject.matches()[0];
testObject.replace(objectRoRemove);
return voidPromise.then(() => {
assert.ok(target.calledOnce);
assert.deepEqual([{ elements: [objectRoRemove], removed: true }], target.args[0]);
});
});
test('replaceAll should remove all file matches', function () {
const voidPromise = Promise.resolve(null);
instantiationService.stubPromise(IReplaceService, 'replace', voidPromise);
const testObject = aSearchResult();
testObject.add([
aRawMatch('file://c:/1',
new TextSearchMatch('preview 1', lineOneRange)),
aRawMatch('file://c:/2',
new TextSearchMatch('preview 2', lineOneRange))]);
testObject.replaceAll(null!);
return voidPromise.then(() => assert.ok(testObject.isEmpty()));
});
function aFileMatch(path: string, searchResult?: SearchResult, ...lineMatches: ITextSearchMatch[]): FileMatch {
const rawMatch: IFileMatch = {
resource: URI.file('/' + path),
results: lineMatches
};
return instantiationService.createInstance(FileMatch, null, null, null, searchResult, rawMatch);
}
function aSearchResult(): SearchResult {
const searchModel = instantiationService.createInstance(SearchModel);
searchModel.searchResult.query = { type: 1, folderQueries: [{ folder: URI.parse('file://c:/') }] };
return searchModel.searchResult;
}
function aRawMatch(resource: string, ...results: ITextSearchMatch[]): IFileMatch {
return { resource: URI.parse(resource), results };
}
function stubModelService(instantiationService: TestInstantiationService): IModelService {
instantiationService.stub(IConfigurationService, new TestConfigurationService());
return instantiationService.createInstance(ModelServiceImpl);
}
});