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
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
13
src/vs/workbench/contrib/search/browser/media/replace.svg
Normal 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 |
@@ -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 |
@@ -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%;
|
||||
}
|
||||
398
src/vs/workbench/contrib/search/browser/media/searchview.css
Normal 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;
|
||||
}
|
||||
@@ -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 |
3
src/vs/workbench/contrib/search/browser/media/stop.svg
Normal 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 |
249
src/vs/workbench/contrib/search/browser/openAnythingHandler.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
334
src/vs/workbench/contrib/search/browser/openFileHandler.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
231
src/vs/workbench/contrib/search/browser/openSymbolHandler.ts
Normal 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()
|
||||
};
|
||||
}
|
||||
}
|
||||
212
src/vs/workbench/contrib/search/browser/patternInputWidget.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
213
src/vs/workbench/contrib/search/browser/replaceService.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
774
src/vs/workbench/contrib/search/browser/search.contribution.ts
Normal 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
|
||||
776
src/vs/workbench/contrib/search/browser/searchActions.ts
Normal 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();
|
||||
});
|
||||
};
|
||||
70
src/vs/workbench/contrib/search/browser/searchPanel.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
383
src/vs/workbench/contrib/search/browser/searchResultsView.ts
Normal 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 {
|
||||
}
|
||||
}
|
||||
1717
src/vs/workbench/contrib/search/browser/searchView.ts
Normal file
45
src/vs/workbench/contrib/search/browser/searchViewlet.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
557
src/vs/workbench/contrib/search/browser/searchWidget.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
45
src/vs/workbench/contrib/search/common/constants.ts
Normal 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);
|
||||
499
src/vs/workbench/contrib/search/common/queryBuilder.ts
Normal 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, '');
|
||||
}
|
||||
37
src/vs/workbench/contrib/search/common/replace.ts
Normal 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>;
|
||||
}
|
||||
95
src/vs/workbench/contrib/search/common/search.ts
Normal 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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
1128
src/vs/workbench/contrib/search/common/searchModel.ts
Normal 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];
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
1044
src/vs/workbench/contrib/search/test/common/queryBuilder.test.ts
Normal file
329
src/vs/workbench/contrib/search/test/common/searchModel.test.ts
Normal 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);
|
||||
}
|
||||
|
||||
});
|
||||
340
src/vs/workbench/contrib/search/test/common/searchResult.test.ts
Normal 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);
|
||||
}
|
||||
});
|
||||