Skip to content

Commit a3a8d6f

Browse files
devvaannshabose
authored andcommitted
fix: optimize the snippet code hints to prevent checking for hints on every keystroke
1 parent 4fbd401 commit a3a8d6f

File tree

4 files changed

+143
-17
lines changed

4 files changed

+143
-17
lines changed

src/extensionsIntegrated/CustomSnippets/driver.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ define(function (require, exports, module) {
4545

4646
if (shouldAddSnippetToList(snippetData)) {
4747
Global.SnippetHintsList.push(snippetData);
48+
Helper.rebuildOptimizedStructures();
4849
Helper.clearAllInputFields();
4950
Helper.toggleSaveButtonDisability();
5051

@@ -98,6 +99,7 @@ define(function (require, exports, module) {
9899
// update the snippet in the list
99100
if (snippetIndex !== -1) {
100101
Global.SnippetHintsList[snippetIndex] = editedData;
102+
Helper.rebuildOptimizedStructures();
101103

102104
// save to file storage
103105
SnippetsState.saveSnippetsToState()

src/extensionsIntegrated/CustomSnippets/helper.js

Lines changed: 134 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,77 @@ define(function (require, exports, module) {
5353
"F12"
5454
];
5555

56+
// Optimized data structures for fast snippet lookups
57+
let snippetsByLanguage = new Map();
58+
let snippetsByAbbreviation = new Map();
59+
let allSnippetsOptimized = [];
60+
61+
/**
62+
* Preprocesses a snippet to add optimized lookup properties
63+
* @param {Object} snippet - The original snippet object
64+
* @returns {Object} - The snippet with added optimization properties
65+
*/
66+
function preprocessSnippet(snippet) {
67+
const optimizedSnippet = { ...snippet };
68+
69+
// pre-compute lowercase abbreviation for faster matching
70+
optimizedSnippet.abbreviationLower = snippet.abbreviation.toLowerCase();
71+
72+
// parse and create a Set of supported extensions for O(1) lookup
73+
if (snippet.fileExtension.toLowerCase() === "all") {
74+
optimizedSnippet.supportedLangSet = new Set(["all"]);
75+
optimizedSnippet.supportsAllLanguages = true;
76+
} else {
77+
const extensions = snippet.fileExtension
78+
.toLowerCase()
79+
.split(",")
80+
.map(ext => ext.trim())
81+
.filter(ext => ext);
82+
optimizedSnippet.supportedLangSet = new Set(extensions);
83+
optimizedSnippet.supportsAllLanguages = false;
84+
}
85+
86+
return optimizedSnippet;
87+
}
88+
89+
/**
90+
* Rebuilds optimized data structures from the current snippet list
91+
* we call this function whenever snippets are loaded, added, modified, or deleted
92+
* i.e. whenever the snippetList is updated
93+
*/
94+
function rebuildOptimizedStructures() {
95+
// clear existing structures
96+
snippetsByLanguage.clear();
97+
snippetsByAbbreviation.clear();
98+
allSnippetsOptimized.length = 0;
99+
100+
// Process each snippet
101+
Global.SnippetHintsList.forEach(snippet => {
102+
const optimizedSnippet = preprocessSnippet(snippet);
103+
allSnippetsOptimized.push(optimizedSnippet);
104+
105+
// Index by abbreviation (lowercase) for exact matches
106+
snippetsByAbbreviation.set(optimizedSnippet.abbreviationLower, optimizedSnippet);
107+
108+
// Index by supported languages/extensions
109+
if (optimizedSnippet.supportsAllLanguages) {
110+
// Add to a special "all" key for universal snippets
111+
if (!snippetsByLanguage.has("all")) {
112+
snippetsByLanguage.set("all", new Set());
113+
}
114+
snippetsByLanguage.get("all").add(optimizedSnippet);
115+
} else {
116+
// Add to each supported extension
117+
optimizedSnippet.supportedLangSet.forEach(ext => {
118+
if (!snippetsByLanguage.has(ext)) {
119+
snippetsByLanguage.set(ext, new Set());
120+
}
121+
snippetsByLanguage.get(ext).add(optimizedSnippet);
122+
});
123+
}
124+
});
125+
}
126+
56127
/**
57128
* map the language IDs to their file extensions for snippet matching
58129
* this is needed because we expect the user to enter file extensions and not the file type inside the input field
@@ -237,12 +308,37 @@ define(function (require, exports, module) {
237308
* Checks if a snippet is supported in the given language context
238309
* Falls back to file extension matching if language mapping isn't available
239310
*
240-
* @param {Object} snippet - The snippet object
311+
* @param {Object} snippet - The snippet object (optimized or regular)
241312
* @param {string|null} languageContext - The current language context
242313
* @param {Editor} editor - The editor instance for fallback
243314
* @returns {boolean} - True if the snippet is supported
244315
*/
245316
function isSnippetSupportedInLanguageContext(snippet, languageContext, editor) {
317+
// first we need to try the optimizedSnippet
318+
if (snippet.supportsAllLanguages !== undefined) {
319+
if (snippet.supportsAllLanguages) {
320+
return true;
321+
}
322+
323+
if (languageContext) {
324+
const effectiveExtension = mapLanguageToExtension(languageContext);
325+
// if we have a proper mapping (starts with .), use language context matching
326+
if (effectiveExtension.startsWith(".")) {
327+
return snippet.supportedLangSet.has(effectiveExtension);
328+
}
329+
}
330+
331+
// this is just a fallback if language context matching failed
332+
// file extension matching
333+
if (editor) {
334+
const fileExtension = getCurrentFileExtension(editor);
335+
return isSnippetSupportedInFile(snippet, fileExtension);
336+
}
337+
338+
return false;
339+
}
340+
341+
// for non-optimized snippets
246342
if (snippet.fileExtension.toLowerCase() === "all") {
247343
return true;
248344
}
@@ -303,12 +399,12 @@ define(function (require, exports, module) {
303399
const queryLower = query.toLowerCase();
304400
const languageContext = getCurrentLanguageContext(editor);
305401

306-
return Global.SnippetHintsList.some((snippet) => {
307-
if (snippet.abbreviation.toLowerCase() === queryLower) {
308-
return isSnippetSupportedInLanguageContext(snippet, languageContext, editor);
309-
}
310-
return false;
311-
});
402+
const snippet = snippetsByAbbreviation.get(queryLower);
403+
if (snippet) {
404+
return isSnippetSupportedInLanguageContext(snippet, languageContext, editor);
405+
}
406+
407+
return false;
312408
}
313409

314410
/**
@@ -321,21 +417,41 @@ define(function (require, exports, module) {
321417
const queryLower = query.toLowerCase();
322418
const languageContext = getCurrentLanguageContext(editor);
323419

324-
const matchingSnippets = Global.SnippetHintsList.filter((snippet) => {
325-
if (snippet.abbreviation.toLowerCase().startsWith(queryLower)) {
326-
return isSnippetSupportedInLanguageContext(snippet, languageContext, editor);
420+
// Get the candidate snippets for the current language/extension
421+
let candidateSnippets = new Set();
422+
423+
// Add universal snippets (support "all" languages)
424+
const universalSnippets = snippetsByLanguage.get("all");
425+
if (universalSnippets) {
426+
universalSnippets.forEach(snippet => candidateSnippets.add(snippet));
427+
}
428+
429+
// Add language-specific snippets
430+
if (languageContext) {
431+
const effectiveExtension = mapLanguageToExtension(languageContext);
432+
if (effectiveExtension.startsWith(".")) {
433+
const languageSnippets = snippetsByLanguage.get(effectiveExtension);
434+
if (languageSnippets) {
435+
languageSnippets.forEach(snippet => candidateSnippets.add(snippet));
436+
}
327437
}
328-
return false;
438+
}
439+
440+
// Fallback: if we can't determine language, check all snippets
441+
if (candidateSnippets.size === 0) {
442+
candidateSnippets = new Set(allSnippetsOptimized);
443+
}
444+
445+
// Filter candidates by prefix match using pre-computed lowercase abbreviations
446+
const matchingSnippets = Array.from(candidateSnippets).filter((snippet) => {
447+
return snippet.abbreviationLower.startsWith(queryLower);
329448
});
330449

331450
// sort snippets so that the exact matches will appear over the partial matches
332451
return matchingSnippets.sort((a, b) => {
333-
const aLower = a.abbreviation.toLowerCase();
334-
const bLower = b.abbreviation.toLowerCase();
335-
336452
// check if either is an exact match
337-
const aExact = aLower === queryLower;
338-
const bExact = bLower === queryLower;
453+
const aExact = a.abbreviationLower === queryLower;
454+
const bExact = b.abbreviationLower === queryLower;
339455

340456
// because exact matches appear first
341457
if (aExact && !bExact) {
@@ -345,7 +461,7 @@ define(function (require, exports, module) {
345461
return 1;
346462
}
347463

348-
return aLower.localeCompare(bLower);
464+
return a.abbreviationLower.localeCompare(b.abbreviationLower);
349465
});
350466
}
351467

@@ -834,6 +950,7 @@ define(function (require, exports, module) {
834950
exports.getCurrentLanguageContext = getCurrentLanguageContext;
835951
exports.getCurrentFileExtension = getCurrentFileExtension;
836952
exports.mapLanguageToExtension = mapLanguageToExtension;
953+
exports.rebuildOptimizedStructures = rebuildOptimizedStructures;
837954
exports.isSnippetSupportedInLanguageContext = isSnippetSupportedInLanguageContext;
838955
exports.isSnippetSupportedInFile = isSnippetSupportedInFile;
839956
exports.hasExactMatchingSnippet = hasExactMatchingSnippet;

src/extensionsIntegrated/CustomSnippets/snippetsList.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,7 @@ define(function (require, exports, module) {
171171
Metrics.countEvent(Metrics.EVENT_TYPE.EDITOR, "snipt", `del.${fileCategory}`);
172172

173173
Global.SnippetHintsList.splice(index, 1); // removes it from the actual array
174+
Helper.rebuildOptimizedStructures();
174175

175176
// save to file storage
176177
SnippetsState.saveSnippetsToState()

src/extensionsIntegrated/CustomSnippets/snippetsState.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ define(function (require, exports, module) {
2424
const FileSystem = require("filesystem/FileSystem");
2525
const FileUtils = require("file/FileUtils");
2626
const FileSystemError = require("filesystem/FileSystemError");
27+
const Helper = require("./helper");
2728

2829
const SNIPPETS_FILE_PATH = brackets.app.getApplicationSupportDirectory() + "/customSnippets.json";
2930

@@ -48,24 +49,29 @@ define(function (require, exports, module) {
4849
// no snippets are present
4950
Global.SnippetHintsList = [];
5051
}
52+
// rebuild the optimized data structures after loading snippets
53+
Helper.rebuildOptimizedStructures();
5154
resolve();
5255
} catch (error) {
5356
logger.reportError(
5457
error,
5558
"Custom Snippets: Failed to parse snippets JSON file. File might be corrupted."
5659
);
5760
Global.SnippetHintsList = []; // fallback
61+
Helper.rebuildOptimizedStructures();
5862
resolve();
5963
}
6064
})
6165
.fail(function (error) {
6266
if (error === FileSystemError.NOT_FOUND) {
6367
// file is not present, empty array
6468
Global.SnippetHintsList = [];
69+
Helper.rebuildOptimizedStructures();
6570
resolve();
6671
} else {
6772
logger.reportError(error, "Custom Snippets: unexpected file system error loading snippets");
6873
Global.SnippetHintsList = [];
74+
Helper.rebuildOptimizedStructures();
6975
resolve();
7076
}
7177
});

0 commit comments

Comments
 (0)