@theia/core 1.70.0-next.7 → 1.70.0-next.71

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (194) hide show
  1. package/README.md +3 -3
  2. package/lib/browser/about-dialog.d.ts.map +1 -1
  3. package/lib/browser/about-dialog.js +3 -1
  4. package/lib/browser/about-dialog.js.map +1 -1
  5. package/lib/browser/catalog.json +83 -7
  6. package/lib/browser/common-frontend-contribution.d.ts +0 -2
  7. package/lib/browser/common-frontend-contribution.d.ts.map +1 -1
  8. package/lib/browser/common-frontend-contribution.js +39 -15
  9. package/lib/browser/common-frontend-contribution.js.map +1 -1
  10. package/lib/browser/frontend-application-module.d.ts.map +1 -1
  11. package/lib/browser/frontend-application-module.js +1 -0
  12. package/lib/browser/frontend-application-module.js.map +1 -1
  13. package/lib/browser/hover-service.d.ts.map +1 -1
  14. package/lib/browser/hover-service.js +5 -3
  15. package/lib/browser/hover-service.js.map +1 -1
  16. package/lib/browser/markdown-rendering/markdown-renderer.d.ts +1 -1
  17. package/lib/browser/markdown-rendering/markdown-renderer.d.ts.map +1 -1
  18. package/lib/browser/menu/browser-menu-plugin.d.ts.map +1 -1
  19. package/lib/browser/menu/browser-menu-plugin.js +9 -1
  20. package/lib/browser/menu/browser-menu-plugin.js.map +1 -1
  21. package/lib/browser/quick-input/quick-input-service.spec.js +53 -9
  22. package/lib/browser/quick-input/quick-input-service.spec.js.map +1 -1
  23. package/lib/browser/saveable-service.d.ts +24 -1
  24. package/lib/browser/saveable-service.d.ts.map +1 -1
  25. package/lib/browser/saveable-service.js +34 -3
  26. package/lib/browser/saveable-service.js.map +1 -1
  27. package/lib/browser/tree/fuzzy-search.d.ts +1 -60
  28. package/lib/browser/tree/fuzzy-search.d.ts.map +1 -1
  29. package/lib/browser/tree/fuzzy-search.js +3 -58
  30. package/lib/browser/tree/fuzzy-search.js.map +1 -1
  31. package/lib/browser/tree/tree-view-welcome-widget.d.ts.map +1 -1
  32. package/lib/browser/tree/tree-view-welcome-widget.js +1 -2
  33. package/lib/browser/tree/tree-view-welcome-widget.js.map +1 -1
  34. package/lib/browser/widgets/select-component.d.ts +1 -0
  35. package/lib/browser/widgets/select-component.d.ts.map +1 -1
  36. package/lib/browser/widgets/select-component.js +30 -0
  37. package/lib/browser/widgets/select-component.js.map +1 -1
  38. package/lib/browser/window/default-window-service.d.ts +2 -1
  39. package/lib/browser/window/default-window-service.d.ts.map +1 -1
  40. package/lib/browser/window/default-window-service.js +5 -1
  41. package/lib/browser/window/default-window-service.js.map +1 -1
  42. package/lib/browser/window/test/mock-window-service.d.ts +2 -1
  43. package/lib/browser/window/test/mock-window-service.d.ts.map +1 -1
  44. package/lib/browser/window/test/mock-window-service.js +2 -1
  45. package/lib/browser/window/test/mock-window-service.js.map +1 -1
  46. package/lib/browser/window/window-service.d.ts +8 -1
  47. package/lib/browser/window/window-service.d.ts.map +1 -1
  48. package/lib/common/fuzzy-match-utils.d.ts +27 -0
  49. package/lib/common/fuzzy-match-utils.d.ts.map +1 -0
  50. package/lib/common/fuzzy-match-utils.js +96 -0
  51. package/lib/common/fuzzy-match-utils.js.map +1 -0
  52. package/lib/common/fuzzy-match-utils.spec.d.ts +2 -0
  53. package/lib/common/fuzzy-match-utils.spec.d.ts.map +1 -0
  54. package/lib/common/fuzzy-match-utils.spec.js +109 -0
  55. package/lib/common/fuzzy-match-utils.spec.js.map +1 -0
  56. package/lib/common/fuzzy-search.d.ts +63 -0
  57. package/lib/common/fuzzy-search.d.ts.map +1 -0
  58. package/lib/common/fuzzy-search.js +101 -0
  59. package/lib/common/fuzzy-search.js.map +1 -0
  60. package/lib/common/fuzzy-search.spec.d.ts.map +1 -0
  61. package/lib/{browser/tree → common}/fuzzy-search.spec.js +20 -1
  62. package/lib/common/fuzzy-search.spec.js.map +1 -0
  63. package/lib/common/logger-sanitizer.d.ts +9 -0
  64. package/lib/common/logger-sanitizer.d.ts.map +1 -1
  65. package/lib/common/logger-sanitizer.js +26 -3
  66. package/lib/common/logger-sanitizer.js.map +1 -1
  67. package/lib/common/logger-sanitizer.spec.js +41 -0
  68. package/lib/common/logger-sanitizer.spec.js.map +1 -1
  69. package/lib/common/preferences/injectable-preference-proxy.d.ts +5 -3
  70. package/lib/common/preferences/injectable-preference-proxy.d.ts.map +1 -1
  71. package/lib/common/preferences/injectable-preference-proxy.js +9 -6
  72. package/lib/common/preferences/injectable-preference-proxy.js.map +1 -1
  73. package/lib/common/preferences/preference-configurations.d.ts +2 -2
  74. package/lib/common/preferences/preference-configurations.d.ts.map +1 -1
  75. package/lib/common/preferences/preference-configurations.js +1 -1
  76. package/lib/common/preferences/preference-configurations.js.map +1 -1
  77. package/lib/common/preferences/preference-provider-impl.d.ts +4 -3
  78. package/lib/common/preferences/preference-provider-impl.d.ts.map +1 -1
  79. package/lib/common/preferences/preference-provider-impl.js +9 -6
  80. package/lib/common/preferences/preference-provider-impl.js.map +1 -1
  81. package/lib/common/preferences/preference-proxy.d.ts +4 -2
  82. package/lib/common/preferences/preference-proxy.d.ts.map +1 -1
  83. package/lib/common/preferences/preference-proxy.js +4 -5
  84. package/lib/common/preferences/preference-proxy.js.map +1 -1
  85. package/lib/common/preferences/preference-service.d.ts +4 -3
  86. package/lib/common/preferences/preference-service.d.ts.map +1 -1
  87. package/lib/common/preferences/preference-service.js +13 -10
  88. package/lib/common/preferences/preference-service.js.map +1 -1
  89. package/lib/common/quick-pick-service.d.ts +18 -2
  90. package/lib/common/quick-pick-service.d.ts.map +1 -1
  91. package/lib/common/quick-pick-service.js +53 -8
  92. package/lib/common/quick-pick-service.js.map +1 -1
  93. package/lib/common/uri.js +1 -1
  94. package/lib/common/uri.js.map +1 -1
  95. package/lib/electron-browser/menu/electron-main-menu-factory.d.ts.map +1 -1
  96. package/lib/electron-browser/menu/electron-main-menu-factory.js +4 -6
  97. package/lib/electron-browser/menu/electron-main-menu-factory.js.map +1 -1
  98. package/lib/electron-browser/menu/electron-menu-contribution.js +3 -3
  99. package/lib/electron-browser/menu/electron-menu-contribution.js.map +1 -1
  100. package/lib/electron-browser/messaging/electron-local-ws-connection-source.d.ts +2 -0
  101. package/lib/electron-browser/messaging/electron-local-ws-connection-source.d.ts.map +1 -1
  102. package/lib/electron-browser/messaging/electron-local-ws-connection-source.js +5 -3
  103. package/lib/electron-browser/messaging/electron-local-ws-connection-source.js.map +1 -1
  104. package/lib/electron-browser/preload.js +2 -2
  105. package/lib/electron-browser/preload.js.map +1 -1
  106. package/lib/electron-browser/window/electron-secondary-window-service.d.ts +4 -0
  107. package/lib/electron-browser/window/electron-secondary-window-service.d.ts.map +1 -1
  108. package/lib/electron-browser/window/electron-secondary-window-service.js +32 -0
  109. package/lib/electron-browser/window/electron-secondary-window-service.js.map +1 -1
  110. package/lib/electron-browser/window/electron-window-module.d.ts +1 -0
  111. package/lib/electron-browser/window/electron-window-module.d.ts.map +1 -1
  112. package/lib/electron-browser/window/electron-window-module.js +4 -0
  113. package/lib/electron-browser/window/electron-window-module.js.map +1 -1
  114. package/lib/electron-browser/window/electron-window-service.d.ts +3 -2
  115. package/lib/electron-browser/window/electron-window-service.d.ts.map +1 -1
  116. package/lib/electron-browser/window/electron-window-service.js +7 -4
  117. package/lib/electron-browser/window/electron-window-service.js.map +1 -1
  118. package/lib/electron-browser/window/window-zoom-action-bar.d.ts +21 -0
  119. package/lib/electron-browser/window/window-zoom-action-bar.d.ts.map +1 -0
  120. package/lib/electron-browser/window/window-zoom-action-bar.js +71 -0
  121. package/lib/electron-browser/window/window-zoom-action-bar.js.map +1 -0
  122. package/lib/electron-browser/window/window-zoom-status-bar-item.d.ts +14 -0
  123. package/lib/electron-browser/window/window-zoom-status-bar-item.d.ts.map +1 -0
  124. package/lib/electron-browser/window/window-zoom-status-bar-item.js +87 -0
  125. package/lib/electron-browser/window/window-zoom-status-bar-item.js.map +1 -0
  126. package/lib/electron-common/electron-api.d.ts +1 -1
  127. package/lib/electron-common/electron-api.d.ts.map +1 -1
  128. package/lib/electron-common/electron-main-window-service.d.ts +2 -1
  129. package/lib/electron-common/electron-main-window-service.d.ts.map +1 -1
  130. package/lib/electron-common/electron-window-preferences.d.ts +5 -2
  131. package/lib/electron-common/electron-window-preferences.d.ts.map +1 -1
  132. package/lib/electron-common/electron-window-preferences.js +16 -10
  133. package/lib/electron-common/electron-window-preferences.js.map +1 -1
  134. package/lib/electron-main/electron-api-main.d.ts.map +1 -1
  135. package/lib/electron-main/electron-api-main.js +14 -2
  136. package/lib/electron-main/electron-api-main.js.map +1 -1
  137. package/lib/electron-main/electron-main-application.d.ts +1 -0
  138. package/lib/electron-main/electron-main-application.d.ts.map +1 -1
  139. package/lib/electron-main/electron-main-application.js +6 -0
  140. package/lib/electron-main/electron-main-application.js.map +1 -1
  141. package/lib/electron-main/electron-main-window-service-impl.d.ts +2 -1
  142. package/lib/electron-main/electron-main-window-service-impl.d.ts.map +1 -1
  143. package/lib/electron-main/electron-main-window-service-impl.js +6 -2
  144. package/lib/electron-main/electron-main-window-service-impl.js.map +1 -1
  145. package/package.json +7 -7
  146. package/src/browser/about-dialog.tsx +3 -1
  147. package/src/browser/common-frontend-contribution.ts +36 -17
  148. package/src/browser/frontend-application-module.ts +2 -1
  149. package/src/browser/hover-service.ts +5 -3
  150. package/src/browser/markdown-rendering/markdown-renderer.ts +1 -1
  151. package/src/browser/menu/browser-menu-plugin.ts +9 -1
  152. package/src/browser/quick-input/quick-input-service.spec.ts +58 -9
  153. package/src/browser/saveable-service.ts +56 -4
  154. package/src/browser/style/hover-service.css +1 -0
  155. package/src/browser/style/index.css +1 -1
  156. package/src/browser/tree/fuzzy-search.ts +2 -120
  157. package/src/browser/tree/tree-view-welcome-widget.tsx +1 -2
  158. package/src/browser/widgets/select-component.tsx +39 -2
  159. package/src/browser/window/default-window-service.ts +6 -1
  160. package/src/browser/window/test/mock-window-service.ts +2 -1
  161. package/src/browser/window/window-service.ts +9 -1
  162. package/src/common/fuzzy-match-utils.spec.ts +141 -0
  163. package/src/common/fuzzy-match-utils.ts +78 -0
  164. package/src/{browser/tree → common}/fuzzy-search.spec.ts +23 -1
  165. package/src/common/fuzzy-search.ts +158 -0
  166. package/src/common/i18n/nls.metadata.json +25379 -24577
  167. package/src/common/logger-sanitizer.spec.ts +54 -1
  168. package/src/common/logger-sanitizer.ts +38 -3
  169. package/src/common/preferences/injectable-preference-proxy.ts +6 -3
  170. package/src/common/preferences/preference-configurations.ts +2 -2
  171. package/src/common/preferences/preference-provider-impl.ts +6 -3
  172. package/src/common/preferences/preference-proxy.ts +6 -4
  173. package/src/common/preferences/preference-service.ts +6 -3
  174. package/src/common/quick-pick-service.ts +65 -11
  175. package/src/common/uri.ts +1 -1
  176. package/src/electron-browser/menu/electron-main-menu-factory.ts +4 -6
  177. package/src/electron-browser/menu/electron-menu-contribution.ts +4 -4
  178. package/src/electron-browser/messaging/electron-local-ws-connection-source.ts +4 -2
  179. package/src/electron-browser/preload.ts +2 -2
  180. package/src/electron-browser/style/window-zoom-action-bar.css +57 -0
  181. package/src/electron-browser/window/electron-secondary-window-service.ts +32 -1
  182. package/src/electron-browser/window/electron-window-module.ts +4 -0
  183. package/src/electron-browser/window/electron-window-service.ts +10 -7
  184. package/src/electron-browser/window/window-zoom-action-bar.tsx +115 -0
  185. package/src/electron-browser/window/window-zoom-status-bar-item.ts +81 -0
  186. package/src/electron-common/electron-api.ts +1 -1
  187. package/src/electron-common/electron-main-window-service.ts +2 -1
  188. package/src/electron-common/electron-window-preferences.ts +19 -11
  189. package/src/electron-main/electron-api-main.ts +12 -2
  190. package/src/electron-main/electron-main-application.ts +7 -0
  191. package/src/electron-main/electron-main-window-service-impl.ts +7 -2
  192. package/lib/browser/tree/fuzzy-search.spec.d.ts.map +0 -1
  193. package/lib/browser/tree/fuzzy-search.spec.js.map +0 -1
  194. /package/lib/{browser/tree → common}/fuzzy-search.spec.d.ts +0 -0
@@ -39,8 +39,16 @@ export interface WindowService {
39
39
  /**
40
40
  * Opens a new default window.
41
41
  * - In electron and in the browser it will open the default window without a pre-defined content.
42
+ * @returns a window identifier that can be passed to {@link closeWindow}, or -1 if not supported.
42
43
  */
43
- openNewDefaultWindow(params?: WindowReloadOptions): void;
44
+ openNewDefaultWindow(params?: WindowReloadOptions): Promise<number>;
45
+
46
+ /**
47
+ * Closes a window previously opened by {@link openNewDefaultWindow}.
48
+ * @param windowId the identifier returned by {@link openNewDefaultWindow}.
49
+ * No-op if the window does not exist or the platform does not support it.
50
+ */
51
+ closeWindow(windowId: number): void;
44
52
 
45
53
  /**
46
54
  * Reveal and focuses the current window
@@ -0,0 +1,141 @@
1
+ // *****************************************************************************
2
+ // Copyright (C) 2026 EclipseSource and others.
3
+ //
4
+ // This program and the accompanying materials are made available under the
5
+ // terms of the Eclipse Public License v. 2.0 which is available at
6
+ // http://www.eclipse.org/legal/epl-2.0.
7
+ //
8
+ // This Source Code may also be made available under the following Secondary
9
+ // Licenses when the conditions for such availability set forth in the Eclipse
10
+ // Public License v. 2.0 are satisfied: GNU General Public License, version 2
11
+ // with the GNU Classpath Exception which is available at
12
+ // https://www.gnu.org/software/classpath/license.html.
13
+ //
14
+ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15
+ // *****************************************************************************
16
+
17
+ import { expect } from 'chai';
18
+ import { findSubstringIndex, hasPrefixMatch, hasSubstringMatch, matchRank } from './fuzzy-match-utils';
19
+
20
+ describe('fuzzy-match-utils', () => {
21
+
22
+ describe('#findSubstringIndex', () => {
23
+
24
+ it('should return the index of a case-insensitive substring match', () => {
25
+ expect(findSubstringIndex('fontSize', 'font')).to.equal(0);
26
+ expect(findSubstringIndex('setFont', 'font')).to.equal(3);
27
+ });
28
+
29
+ it('should be case-insensitive', () => {
30
+ expect(findSubstringIndex('FontSize', 'font')).to.equal(0);
31
+ expect(findSubstringIndex('fontSize', 'Font')).to.equal(0);
32
+ });
33
+
34
+ it('should return -1 when pattern is not a substring', () => {
35
+ expect(findSubstringIndex('reformatting', 'font')).to.equal(-1);
36
+ });
37
+
38
+ it('should return 0 for empty pattern', () => {
39
+ expect(findSubstringIndex('anything', '')).to.equal(0);
40
+ });
41
+
42
+ });
43
+
44
+ describe('#hasSubstringMatch', () => {
45
+
46
+ it('should return true for exact substring match', () => {
47
+ expect(hasSubstringMatch('fontSize', 'font')).to.be.true;
48
+ });
49
+
50
+ it('should be case-insensitive', () => {
51
+ expect(hasSubstringMatch('FontSize', 'font')).to.be.true;
52
+ expect(hasSubstringMatch('fontSize', 'Font')).to.be.true;
53
+ });
54
+
55
+ it('should return false when pattern is not a substring', () => {
56
+ expect(hasSubstringMatch('reformatting', 'font')).to.be.false;
57
+ });
58
+
59
+ it('should return true for empty pattern', () => {
60
+ expect(hasSubstringMatch('anything', '')).to.be.true;
61
+ });
62
+
63
+ it('should return true when text equals pattern', () => {
64
+ expect(hasSubstringMatch('font', 'font')).to.be.true;
65
+ });
66
+
67
+ });
68
+
69
+ describe('#hasPrefixMatch', () => {
70
+
71
+ it('should match simple prefix', () => {
72
+ expect(hasPrefixMatch('fontSize', 'font')).to.be.true;
73
+ });
74
+
75
+ it('should match segmented prefix across punctuation', () => {
76
+ expect(hasPrefixMatch('workspace-server', 'works-ser')).to.be.true;
77
+ });
78
+
79
+ it('should match when query parts skip segments', () => {
80
+ expect(hasPrefixMatch('workspace-backend-service', 'works-ser')).to.be.true;
81
+ });
82
+
83
+ it('should not match when first segment does not match', () => {
84
+ expect(hasPrefixMatch('backend-workspace-service', 'works-ser')).to.be.false;
85
+ });
86
+
87
+ it('should be case-insensitive', () => {
88
+ expect(hasPrefixMatch('WorkspaceServer', 'work')).to.be.true;
89
+ });
90
+
91
+ it('should return true for empty pattern', () => {
92
+ expect(hasPrefixMatch('anything', '')).to.be.true;
93
+ });
94
+
95
+ it('should handle various separators', () => {
96
+ expect(hasPrefixMatch('workspace_server', 'works_ser')).to.be.true;
97
+ expect(hasPrefixMatch('workspace.server.ts', 'works.ser')).to.be.true;
98
+ expect(hasPrefixMatch('workspace/server', 'works/ser')).to.be.true;
99
+ });
100
+
101
+ it('should not match when query parts are out of order', () => {
102
+ expect(hasPrefixMatch('service-workspace', 'works-ser')).to.be.false;
103
+ });
104
+
105
+ it('should return true when pattern is only separators', () => {
106
+ expect(hasPrefixMatch('anything', '---')).to.be.true;
107
+ });
108
+
109
+ it('should return false when text is only separators', () => {
110
+ expect(hasPrefixMatch('---', 'abc')).to.be.false;
111
+ });
112
+
113
+ it('should not match when a later query part has no matching segment', () => {
114
+ expect(hasPrefixMatch('workspace-server', 'works-zzz')).to.be.false;
115
+ });
116
+
117
+ });
118
+
119
+ describe('#matchRank', () => {
120
+
121
+ it('should return 0 for prefix matches', () => {
122
+ expect(matchRank('fontSize', 'font')).to.equal(0);
123
+ expect(matchRank('workspace-server', 'works-ser')).to.equal(0);
124
+ });
125
+
126
+ it('should return 1 for substring (non-prefix) matches', () => {
127
+ expect(matchRank('setFont', 'font')).to.equal(1);
128
+ expect(matchRank('base.tsconfig.json', 'con')).to.equal(1);
129
+ });
130
+
131
+ it('should return 2 for fuzzy-only matches', () => {
132
+ expect(matchRank('baconing', 'bcn')).to.equal(2);
133
+ });
134
+
135
+ it('should return 0 for empty pattern', () => {
136
+ expect(matchRank('anything', '')).to.equal(0);
137
+ });
138
+
139
+ });
140
+
141
+ });
@@ -0,0 +1,78 @@
1
+ // *****************************************************************************
2
+ // Copyright (C) 2026 EclipseSource and others.
3
+ //
4
+ // This program and the accompanying materials are made available under the
5
+ // terms of the Eclipse Public License v. 2.0 which is available at
6
+ // http://www.eclipse.org/legal/epl-2.0.
7
+ //
8
+ // This Source Code may also be made available under the following Secondary
9
+ // Licenses when the conditions for such availability set forth in the Eclipse
10
+ // Public License v. 2.0 are satisfied: GNU General Public License, version 2
11
+ // with the GNU Classpath Exception which is available at
12
+ // https://www.gnu.org/software/classpath/license.html.
13
+ //
14
+ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15
+ // *****************************************************************************
16
+
17
+ /**
18
+ * Returns the index of the first case-insensitive substring match of `pattern` in `text`,
19
+ * or `-1` if `pattern` is not a substring. Returns `0` for an empty pattern.
20
+ */
21
+ export function findSubstringIndex(text: string, pattern: string): number {
22
+ if (!pattern) { return 0; }
23
+ return text.toLowerCase().indexOf(pattern.toLowerCase());
24
+ }
25
+
26
+ export function hasSubstringMatch(text: string, pattern: string): boolean {
27
+ return findSubstringIndex(text, pattern) !== -1;
28
+ }
29
+
30
+ const SEGMENT_SEPARATOR = /[\p{P}\s]+/u;
31
+
32
+ /**
33
+ * Tests whether `pattern` is a "prefix match" for `text`, accounting for punctuation-separated segments.
34
+ *
35
+ * The pattern is split on punctuation into query parts. The text is split on punctuation into segments.
36
+ * It is a prefix match when the first query part matches the start of segment 0, and each subsequent
37
+ * query part matches the start of a later segment, in order.
38
+ *
39
+ * Examples:
40
+ * - `hasPrefixMatch("workspace-server", "works-ser")` → true
41
+ * - `hasPrefixMatch("backend-workspace-service", "works-ser")` → false (first segment doesn't match)
42
+ * - `hasPrefixMatch("fontSize", "font")` → true (single-part prefix)
43
+ */
44
+ export function hasPrefixMatch(text: string, pattern: string): boolean {
45
+ if (!pattern) { return true; }
46
+ const queryParts = pattern.toLowerCase().split(SEGMENT_SEPARATOR).filter(Boolean);
47
+ if (queryParts.length === 0) { return true; }
48
+ const textSegments = text.toLowerCase().split(SEGMENT_SEPARATOR).filter(Boolean);
49
+ if (textSegments.length === 0) { return false; }
50
+ // First query part must match the start of the first text segment.
51
+ if (!textSegments[0].startsWith(queryParts[0])) { return false; }
52
+ let segIdx = 1;
53
+ for (let qIdx = 1; qIdx < queryParts.length; qIdx++) {
54
+ let found = false;
55
+ while (segIdx < textSegments.length) {
56
+ if (textSegments[segIdx].startsWith(queryParts[qIdx])) {
57
+ segIdx++;
58
+ found = true;
59
+ break;
60
+ }
61
+ segIdx++;
62
+ }
63
+ if (!found) { return false; }
64
+ }
65
+ return true;
66
+ }
67
+
68
+ /**
69
+ * Returns a numeric rank for how well `pattern` matches `text`:
70
+ * - 0: prefix match (best)
71
+ * - 1: substring match
72
+ * - 2: fuzzy-only match (worst)
73
+ */
74
+ export function matchRank(text: string, pattern: string): number {
75
+ if (hasPrefixMatch(text, pattern)) { return 0; }
76
+ if (hasSubstringMatch(text, pattern)) { return 1; }
77
+ return 2;
78
+ }
@@ -71,7 +71,9 @@ describe('fuzzy-search', () => {
71
71
  });
72
72
 
73
73
  ([
74
- ['con', ['configs', 'base.tsconfig.json', 'tsconfig.json', 'base.nyc.json', 'CONTRIBUTING.MD']],
74
+ // "con" prefix matches (configs, CONTRIBUTING.MD) first, then substring matches (base.tsconfig.json, tsconfig.json), then fuzzy-only (base.nyc.json)
75
+ ['con', ['configs', 'base.tsconfig.json', 'tsconfig.json', 'base.nyc.json', 'CONTRIBUTING.MD'],
76
+ ['configs', 'CONTRIBUTING.MD', 'base.tsconfig.json', 'tsconfig.json', 'base.nyc.json']],
75
77
  ['bcn', ['baconing', 'narwhal', 'a mighty bear canoe'], ['baconing', 'a mighty bear canoe']]
76
78
  ] as ([string, string[], string[]])[]).forEach(test => {
77
79
  const [pattern, items, expected] = test;
@@ -80,6 +82,26 @@ describe('fuzzy-search', () => {
80
82
  });
81
83
  });
82
84
 
85
+ it('should rank substring matches before fuzzy-only matches', async () => {
86
+ const results = await search('font', ['reformatting', 'fontSize', 'fontFamily']);
87
+ // "reformatting" contains f-o-n-t scattered but not as substring; fontSize/fontFamily do
88
+ expectOrder(results, ['fontSize', 'fontFamily']);
89
+ });
90
+
91
+ it('should preserve original order for equal-score fuzzy-only matches', async () => {
92
+ // Both are fuzzy-only matches for "bcn" with different scores from the fuzzy library;
93
+ // when scores are equal, original array order (index) should be preserved.
94
+ const results = await search('ab', ['xab', 'yab']);
95
+ expectOrder(results, ['xab', 'yab']);
96
+ });
97
+
98
+ it('should highlight contiguous substring range instead of scattered fuzzy ranges', async () => {
99
+ const results = await search('works', ['browser-only-workspace-server.ts']);
100
+ expect(results).to.have.length(1);
101
+ // "works" appears at index 13 in "browser-only-workspace-server.ts"
102
+ expect(results[0].ranges).to.deep.equal([{ offset: 13, length: 5 }]);
103
+ });
104
+
83
105
  function expectOrder(actual: FuzzySearch.Match<string>[], expected: string[]): void {
84
106
  expect(actual.map(result => result.item)).to.be.deep.equal(expected);
85
107
  }
@@ -0,0 +1,158 @@
1
+ // *****************************************************************************
2
+ // Copyright (C) 2018 TypeFox and others.
3
+ //
4
+ // This program and the accompanying materials are made available under the
5
+ // terms of the Eclipse Public License v. 2.0 which is available at
6
+ // http://www.eclipse.org/legal/epl-2.0.
7
+ //
8
+ // This Source Code may also be made available under the following Secondary
9
+ // Licenses when the conditions for such availability set forth in the Eclipse
10
+ // Public License v. 2.0 are satisfied: GNU General Public License, version 2
11
+ // with the GNU Classpath Exception which is available at
12
+ // https://www.gnu.org/software/classpath/license.html.
13
+ //
14
+ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15
+ // *****************************************************************************
16
+
17
+ import * as fuzzy from 'fuzzy';
18
+ import { injectable } from 'inversify';
19
+ import { findSubstringIndex, matchRank } from './fuzzy-match-utils';
20
+
21
+ @injectable()
22
+ export class FuzzySearch {
23
+
24
+ private static readonly PRE = '\x01';
25
+ private static readonly POST = '\x02';
26
+
27
+ /**
28
+ * Filters the input and returns with an array that contains all items that match the pattern.
29
+ */
30
+ async filter<T>(input: FuzzySearch.Input<T>): Promise<FuzzySearch.Match<T>[]> {
31
+ return fuzzy.filter(input.pattern, input.items.slice(), {
32
+ pre: FuzzySearch.PRE,
33
+ post: FuzzySearch.POST,
34
+ extract: input.transform
35
+ }).sort((left, right) => this.sortResultsForInput(left, right, input))
36
+ .map(result => this.mapResultForInput(result, input));
37
+ }
38
+
39
+ protected sortResultsForInput<T>(left: fuzzy.FilterResult<T>, right: fuzzy.FilterResult<T>, input: FuzzySearch.Input<T>): number {
40
+ const leftRank = matchRank(input.transform(left.original), input.pattern);
41
+ const rightRank = matchRank(input.transform(right.original), input.pattern);
42
+ if (leftRank !== rightRank) { return leftRank - rightRank; }
43
+ return this.sortResults(left, right);
44
+ }
45
+
46
+ protected sortResults<T>(left: fuzzy.FilterResult<T>, right: fuzzy.FilterResult<T>): number {
47
+ if (right.score !== left.score) { return right.score - left.score; }
48
+ return left.index - right.index;
49
+ }
50
+
51
+ protected mapResultForInput<T>(result: fuzzy.FilterResult<T>, input: FuzzySearch.Input<T>): FuzzySearch.Match<T> {
52
+ const text = input.transform(result.original);
53
+ const substringIndex = input.pattern ? findSubstringIndex(text, input.pattern) : -1;
54
+ if (substringIndex !== -1) {
55
+ return {
56
+ item: result.original,
57
+ ranges: [{ offset: substringIndex, length: input.pattern.length }]
58
+ };
59
+ }
60
+ return this.mapResult(result);
61
+ }
62
+
63
+ protected mapResult<T>(result: fuzzy.FilterResult<T>): FuzzySearch.Match<T> {
64
+ return {
65
+ item: result.original,
66
+ ranges: this.mapRanges(result.string)
67
+ };
68
+ }
69
+
70
+ protected mapRanges(input: string): ReadonlyArray<FuzzySearch.Range> {
71
+ const copy = input.split('').filter(s => s !== '');
72
+ const ranges: FuzzySearch.Range[] = [];
73
+ const validate = (pre: number, post: number) => {
74
+ if (preIndex > postIndex || (preIndex === -1) !== (postIndex === -1)) {
75
+ throw new Error(`Error when trying to map ranges. Escaped string was: '${input}. [${[...input].join('|')}]'`);
76
+ }
77
+ };
78
+ let preIndex = copy.indexOf(FuzzySearch.PRE);
79
+ let postIndex = copy.indexOf(FuzzySearch.POST);
80
+ validate(preIndex, postIndex);
81
+ while (preIndex !== -1 && postIndex !== -1) {
82
+ ranges.push({
83
+ offset: preIndex,
84
+ length: postIndex - preIndex - 1
85
+ });
86
+ copy.splice(postIndex, 1);
87
+ copy.splice(preIndex, 1);
88
+ preIndex = copy.indexOf(FuzzySearch.PRE);
89
+ postIndex = copy.indexOf(FuzzySearch.POST);
90
+ }
91
+ if (ranges.length === 0) {
92
+ throw new Error(`Unexpected zero ranges for match-string: ${input}.`);
93
+ }
94
+ return ranges;
95
+ }
96
+
97
+ }
98
+
99
+ /**
100
+ * Fuzzy searcher.
101
+ */
102
+ export namespace FuzzySearch {
103
+
104
+ /**
105
+ * A range representing the match region.
106
+ */
107
+ export interface Range {
108
+
109
+ /**
110
+ * The zero based offset of the match region.
111
+ */
112
+ readonly offset: number;
113
+
114
+ /**
115
+ * The length of the match region.
116
+ */
117
+ readonly length: number;
118
+ }
119
+
120
+ /**
121
+ * A fuzzy search match.
122
+ */
123
+ export interface Match<T> {
124
+
125
+ /**
126
+ * The original item.
127
+ */
128
+ readonly item: T;
129
+
130
+ /**
131
+ * An array of ranges representing the match regions.
132
+ */
133
+ readonly ranges: ReadonlyArray<Range>;
134
+ }
135
+
136
+ /**
137
+ * The fuzzy search input.
138
+ */
139
+ export interface Input<T> {
140
+
141
+ /**
142
+ * The pattern to match.
143
+ */
144
+ readonly pattern: string;
145
+
146
+ /**
147
+ * The items to filter based on the `pattern`.
148
+ */
149
+ readonly items: ReadonlyArray<T>;
150
+
151
+ /**
152
+ * Function that extracts the string from the inputs which will be used to evaluate the fuzzy matching filter.
153
+ */
154
+ readonly transform: (item: T) => string;
155
+
156
+ }
157
+
158
+ }