@bniladridas/cursor 0.1.7

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 (100) hide show
  1. package/.clang-tidy +28 -0
  2. package/.dockerignore +56 -0
  3. package/.env.example +29 -0
  4. package/.github/CODEOWNERS +2 -0
  5. package/.github/ISSUE_TEMPLATE/blank.md +27 -0
  6. package/.github/ISSUE_TEMPLATE/bug_report.md +33 -0
  7. package/.github/ISSUE_TEMPLATE/feature_request.md +24 -0
  8. package/.github/SECURITY.md +24 -0
  9. package/.github/codeql/codeql-config.yml +8 -0
  10. package/.github/dependabot.yml +14 -0
  11. package/.github/labeler.yml +50 -0
  12. package/.github/packaging/brand-cursor.png +0 -0
  13. package/.github/packaging/database/init.sql +48 -0
  14. package/.github/packaging/docker/Dockerfile +111 -0
  15. package/.github/packaging/docker/docker-compose.yml +56 -0
  16. package/.github/packaging/scripts/preflight.sh +413 -0
  17. package/.github/packaging/scripts/prepare-release.sh +141 -0
  18. package/.github/packaging/scripts/release.sh +22 -0
  19. package/.github/packaging/scripts/setup-git-hooks.sh +73 -0
  20. package/.github/pull_request_template.md +31 -0
  21. package/.github/signed.json +9 -0
  22. package/.github/workflows/README.md +23 -0
  23. package/.github/workflows/ci.yml +181 -0
  24. package/.github/workflows/cla.yml +33 -0
  25. package/.github/workflows/formula-sha.yml +63 -0
  26. package/.github/workflows/issue-response.yml +44 -0
  27. package/.github/workflows/labeler.yml +42 -0
  28. package/.github/workflows/pr-body.yml +49 -0
  29. package/.github/workflows/release.yml +176 -0
  30. package/.github/workflows/security.yml +94 -0
  31. package/.github/workflows/stale.yml +38 -0
  32. package/AGENTS.md +49 -0
  33. package/CHANGELOG.md +3 -0
  34. package/CMakeLists.txt +646 -0
  35. package/Formula/cursor.rb +46 -0
  36. package/LICENSE +201 -0
  37. package/Makefile +28 -0
  38. package/README.md +46 -0
  39. package/cli.js +16 -0
  40. package/include/agent.h +86 -0
  41. package/include/agent_mode.h +17 -0
  42. package/include/memory_manager.h +102 -0
  43. package/include/services/ai_service.h +31 -0
  44. package/include/services/auth_service.h +87 -0
  45. package/include/services/checkpoint_service.h +69 -0
  46. package/include/services/codebase_service.h +38 -0
  47. package/include/services/command_service.h +23 -0
  48. package/include/services/context_service.h +74 -0
  49. package/include/services/database_service.h +56 -0
  50. package/include/services/error_service.h +106 -0
  51. package/include/services/file_service.h +51 -0
  52. package/include/services/git_service.h +29 -0
  53. package/include/services/github_service.h +85 -0
  54. package/include/services/mcp_service.h +85 -0
  55. package/include/services/multi_file_service.h +93 -0
  56. package/include/services/sandbox_service.h +96 -0
  57. package/include/services/theme_service.h +67 -0
  58. package/include/services/web_service.h +52 -0
  59. package/include/utils/config.h +68 -0
  60. package/include/utils/memory_utils.h +79 -0
  61. package/include/utils/platform.h +56 -0
  62. package/include/utils/ui.h +43 -0
  63. package/include/utils/validation.h +63 -0
  64. package/include/utils/version.h.in +17 -0
  65. package/install.js +49 -0
  66. package/package.json +16 -0
  67. package/release/checksums.txt +3 -0
  68. package/release/cursor-linux/cursor_v0.1.7_linux_amd64.tar.gz +0 -0
  69. package/release/cursor-macos/cursor_v0.1.7_darwin_arm64.tar.gz +0 -0
  70. package/release/cursor-windows/cursor__windows_amd64.zip +0 -0
  71. package/src/agent.cpp +2026 -0
  72. package/src/main.cpp +97 -0
  73. package/src/memory_manager.cpp +814 -0
  74. package/src/services/ai_service.cpp +366 -0
  75. package/src/services/auth_service.cpp +779 -0
  76. package/src/services/checkpoint_service.cpp +465 -0
  77. package/src/services/codebase_service.cpp +233 -0
  78. package/src/services/command_service.cpp +82 -0
  79. package/src/services/context_service.cpp +348 -0
  80. package/src/services/database_service.cpp +148 -0
  81. package/src/services/error_service.cpp +438 -0
  82. package/src/services/file_service.cpp +349 -0
  83. package/src/services/git_service.cpp +148 -0
  84. package/src/services/github_service.cpp +435 -0
  85. package/src/services/mcp_service.cpp +481 -0
  86. package/src/services/multi_file_service.cpp +591 -0
  87. package/src/services/sandbox_service.cpp +678 -0
  88. package/src/services/theme_service.cpp +429 -0
  89. package/src/services/web_service.cpp +532 -0
  90. package/src/utils/config.cpp +77 -0
  91. package/src/utils/memory_utils.cpp +93 -0
  92. package/src/utils/ui.cpp +307 -0
  93. package/src/utils/validation.cpp +306 -0
  94. package/src/utils/version.cpp +175 -0
  95. package/tests/e2e/docker-compose.yml +195 -0
  96. package/tests/e2e/run_e2e_tests.sh +70 -0
  97. package/tests/e2e/run_tests_in_docker.sh +115 -0
  98. package/tests/main_test.cpp +16 -0
  99. package/tests/mocks/mock_ollama.py +98 -0
  100. package/tests/mocks/start_nginx.sh +64 -0
@@ -0,0 +1,591 @@
1
+ #include "services/multi_file_service.h"
2
+ #include "utils/platform.h"
3
+ #include "utils/validation.h"
4
+ #include <algorithm>
5
+ #include <filesystem>
6
+ #include <fstream>
7
+ #include <iostream>
8
+ #include <map>
9
+ #include <regex>
10
+ #include <sstream>
11
+
12
+ namespace Services {
13
+
14
+ std::vector<FileMatch>
15
+ MultiFileService::read_many_files(const std::vector<std::string> &paths,
16
+ const MultiFileOptions &options) {
17
+ std::vector<FileMatch> results;
18
+ size_t total_size = 0;
19
+
20
+ for (const auto &path : paths) {
21
+ if (results.size() >= options.max_files) {
22
+ break;
23
+ }
24
+
25
+ try {
26
+ if (std::filesystem::is_directory(path)) {
27
+ auto dir_files = read_directory_files(path, options);
28
+ for (auto &file : dir_files) {
29
+ if (results.size() >= options.max_files ||
30
+ total_size >= options.max_total_size) {
31
+ break;
32
+ }
33
+ total_size += file.size;
34
+ results.push_back(std::move(file));
35
+ }
36
+ } else if (std::filesystem::is_regular_file(path)) {
37
+ if (should_include_file(path, options)) {
38
+ FileMatch match;
39
+ match.file_path = std::filesystem::path(path).string();
40
+ match.relative_path = std::filesystem::relative(path).string();
41
+ match.file_type = get_file_type(path);
42
+
43
+ // Read file content
44
+ std::ifstream file(path);
45
+ if (file.is_open()) {
46
+ std::ostringstream content;
47
+ content << file.rdbuf();
48
+ match.content = content.str();
49
+ match.size = match.content.size();
50
+
51
+ if (match.size <= options.max_file_size &&
52
+ total_size + match.size <= options.max_total_size) {
53
+ total_size += match.size;
54
+ results.push_back(std::move(match));
55
+ }
56
+ }
57
+ }
58
+ }
59
+ } catch (const std::exception &e) {
60
+ std::cerr << "Error processing path " << path << ": " << e.what()
61
+ << std::endl;
62
+ }
63
+ }
64
+
65
+ return results;
66
+ }
67
+
68
+ std::vector<FileMatch>
69
+ MultiFileService::read_directory_files(const std::string &directory,
70
+ const MultiFileOptions &options) {
71
+ std::vector<FileMatch> results;
72
+ size_t total_size = 0;
73
+
74
+ // Load gitignore patterns if enabled
75
+ std::set<std::string> gitignore_patterns;
76
+ if (options.respect_gitignore) {
77
+ gitignore_patterns = load_gitignore_patterns(directory);
78
+ }
79
+
80
+ // Get Git status filtered files if Git filtering is enabled
81
+ std::vector<std::string> git_filtered_files;
82
+ std::set<std::string> git_files_set;
83
+ if (options.git_filter != GitStatusFilter::ALL) {
84
+ git_filtered_files = get_git_status_files(directory, options.git_filter);
85
+ git_files_set = std::set<std::string>(git_filtered_files.begin(),
86
+ git_filtered_files.end());
87
+ }
88
+
89
+ try {
90
+ if (options.recursive) {
91
+ for (const auto &entry :
92
+ std::filesystem::recursive_directory_iterator(directory)) {
93
+ if (results.size() >= options.max_files ||
94
+ total_size >= options.max_total_size) {
95
+ break;
96
+ }
97
+
98
+ if (!entry.is_regular_file()) {
99
+ continue;
100
+ }
101
+
102
+ std::string file_path = entry.path().string();
103
+
104
+ // Check gitignore patterns
105
+ if (options.respect_gitignore &&
106
+ matches_gitignore(file_path, gitignore_patterns)) {
107
+ continue;
108
+ }
109
+
110
+ // Check include/exclude patterns
111
+ if (!should_include_file(file_path, options)) {
112
+ continue;
113
+ }
114
+
115
+ // Create file match
116
+ FileMatch match;
117
+ match.file_path = file_path;
118
+ match.relative_path =
119
+ std::filesystem::relative(file_path, directory).string();
120
+ match.file_type = get_file_type(file_path);
121
+ match.size = entry.file_size();
122
+
123
+ // Skip if file is too large
124
+ if (match.size > options.max_file_size) {
125
+ continue;
126
+ }
127
+
128
+ // Read content
129
+ std::ifstream file(file_path);
130
+ if (file.is_open()) {
131
+ std::ostringstream content;
132
+ content << file.rdbuf();
133
+ match.content = content.str();
134
+
135
+ if (total_size + match.size <= options.max_total_size) {
136
+ total_size += match.size;
137
+ results.push_back(std::move(match));
138
+ }
139
+ }
140
+ }
141
+ } else {
142
+ for (const auto &entry : std::filesystem::directory_iterator(directory)) {
143
+ if (results.size() >= options.max_files ||
144
+ total_size >= options.max_total_size) {
145
+ break;
146
+ }
147
+
148
+ if (!entry.is_regular_file()) {
149
+ continue;
150
+ }
151
+
152
+ std::string file_path = entry.path().string();
153
+
154
+ // Check gitignore patterns
155
+ if (options.respect_gitignore &&
156
+ matches_gitignore(file_path, gitignore_patterns)) {
157
+ continue;
158
+ }
159
+
160
+ // Check include/exclude patterns
161
+ if (!should_include_file(file_path, options)) {
162
+ continue;
163
+ }
164
+
165
+ // Create file match
166
+ FileMatch match;
167
+ match.file_path = file_path;
168
+ match.relative_path =
169
+ std::filesystem::relative(file_path, directory).string();
170
+ match.file_type = get_file_type(file_path);
171
+ match.size = entry.file_size();
172
+
173
+ // Skip if file is too large
174
+ if (match.size > options.max_file_size) {
175
+ continue;
176
+ }
177
+
178
+ // Read content
179
+ std::ifstream file(file_path);
180
+ if (file.is_open()) {
181
+ std::ostringstream content;
182
+ content << file.rdbuf();
183
+ match.content = content.str();
184
+
185
+ if (total_size + match.size <= options.max_total_size) {
186
+ total_size += match.size;
187
+ results.push_back(std::move(match));
188
+ }
189
+ }
190
+ }
191
+ }
192
+ } catch (const std::exception &e) {
193
+ std::cerr << "Error reading directory " << directory << ": " << e.what()
194
+ << std::endl;
195
+ }
196
+
197
+ return results;
198
+ }
199
+
200
+ bool MultiFileService::should_include_file(const std::string &file_path,
201
+ const MultiFileOptions &options) {
202
+ // Check if hidden files should be included
203
+ std::string filename = std::filesystem::path(file_path).filename().string();
204
+ if (!options.include_hidden_files && !filename.empty() &&
205
+ filename[0] == '.') {
206
+ return false;
207
+ }
208
+
209
+ // Check if only text files should be included
210
+ if (options.only_text_files && !is_text_file(file_path)) {
211
+ return false;
212
+ }
213
+
214
+ // Check language filters
215
+ if (!matches_language_filter(file_path, options.language_filters)) {
216
+ return false;
217
+ }
218
+
219
+ // Check exclude patterns first
220
+ for (const auto &pattern : options.exclude_patterns) {
221
+ if (matches_pattern(file_path, pattern)) {
222
+ return false;
223
+ }
224
+ }
225
+
226
+ // Check default exclude patterns
227
+ auto default_excludes = get_default_exclude_patterns();
228
+ for (const auto &pattern : default_excludes) {
229
+ if (matches_pattern(file_path, pattern)) {
230
+ return false;
231
+ }
232
+ }
233
+
234
+ // If no include patterns specified, include by default
235
+ if (options.include_patterns.empty()) {
236
+ return true;
237
+ }
238
+
239
+ // Check include patterns
240
+ for (const auto &pattern : options.include_patterns) {
241
+ if (matches_pattern(file_path, pattern)) {
242
+ return true;
243
+ }
244
+ }
245
+
246
+ return false;
247
+ }
248
+
249
+ std::string MultiFileService::get_file_type(const std::string &file_path) {
250
+ std::string ext = std::filesystem::path(file_path).extension().string();
251
+ std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower);
252
+
253
+ if (ext == ".cpp" || ext == ".cc" || ext == ".cxx" || ext == ".c" ||
254
+ ext == ".h" || ext == ".hpp" || ext == ".hxx") {
255
+ return "C++";
256
+ } else if (ext == ".py") {
257
+ return "Python";
258
+ } else if (ext == ".js" || ext == ".ts") {
259
+ return "JavaScript/TypeScript";
260
+ } else if (ext == ".java") {
261
+ return "Java";
262
+ } else if (ext == ".rs") {
263
+ return "Rust";
264
+ } else if (ext == ".go") {
265
+ return "Go";
266
+ } else if (ext == ".md") {
267
+ return "Markdown";
268
+ } else if (ext == ".json") {
269
+ return "JSON";
270
+ } else if (ext == ".yaml" || ext == ".yml") {
271
+ return "YAML";
272
+ } else if (ext == ".xml") {
273
+ return "XML";
274
+ } else if (ext == ".txt") {
275
+ return "Text";
276
+ } else if (ext == ".sh" || ext == ".bash") {
277
+ return "Shell Script";
278
+ } else {
279
+ return "Unknown";
280
+ }
281
+ }
282
+
283
+ std::set<std::string>
284
+ MultiFileService::load_gitignore_patterns(const std::string &directory) {
285
+ std::set<std::string> patterns;
286
+
287
+ // Add common patterns
288
+ patterns.insert("node_modules/");
289
+ patterns.insert(".git/");
290
+ patterns.insert("build/");
291
+ patterns.insert("dist/");
292
+ patterns.insert(".cache/");
293
+ patterns.insert("*.log");
294
+ patterns.insert("*.tmp");
295
+ patterns.insert("*.swp");
296
+ patterns.insert(".DS_Store");
297
+
298
+ // Try to read .gitignore file
299
+ std::string gitignore_path = directory + "/.gitignore";
300
+ std::ifstream gitignore_file(gitignore_path);
301
+
302
+ if (gitignore_file.is_open()) {
303
+ std::string line;
304
+ while (std::getline(gitignore_file, line)) {
305
+ // Skip empty lines and comments
306
+ if (line.empty() || line[0] == '#') {
307
+ continue;
308
+ }
309
+
310
+ // Trim whitespace
311
+ line.erase(0, line.find_first_not_of(" \t"));
312
+ line.erase(line.find_last_not_of(" \t") + 1);
313
+
314
+ if (!line.empty()) {
315
+ patterns.insert(line);
316
+ }
317
+ }
318
+ }
319
+
320
+ return patterns;
321
+ }
322
+
323
+ bool MultiFileService::matches_gitignore(
324
+ const std::string &file_path, const std::set<std::string> &patterns) {
325
+ for (const auto &pattern : patterns) {
326
+ if (matches_pattern(file_path, pattern)) {
327
+ return true;
328
+ }
329
+ }
330
+ return false;
331
+ }
332
+
333
+ std::string MultiFileService::format_multi_file_content(
334
+ const std::vector<FileMatch> &files,
335
+ const std::string &context_description) {
336
+ std::ostringstream formatted;
337
+
338
+ if (!context_description.empty()) {
339
+ formatted << "[" << context_description << "]\n\n";
340
+ }
341
+
342
+ formatted << "[Multi-file content: " << files.size() << " files]\n\n";
343
+
344
+ // Group files by type
345
+ std::map<std::string, std::vector<const FileMatch *>> files_by_type;
346
+ for (const auto &file : files) {
347
+ files_by_type[file.file_type].push_back(&file);
348
+ }
349
+
350
+ // Output summary
351
+ formatted << "File Summary:\n";
352
+ for (const auto &[type, type_files] : files_by_type) {
353
+ formatted << " " << type << ": " << type_files.size() << " files\n";
354
+ }
355
+ formatted << "\n";
356
+
357
+ // Output file contents
358
+ for (const auto &file : files) {
359
+ formatted << "=== " << file.relative_path << " (" << file.file_type
360
+ << ") ===\n";
361
+ formatted << file.content;
362
+ if (!file.content.empty() && file.content.back() != '\n') {
363
+ formatted << "\n";
364
+ }
365
+ formatted << "\n";
366
+ }
367
+
368
+ return formatted.str();
369
+ }
370
+
371
+ std::vector<std::string> MultiFileService::get_default_exclude_patterns() {
372
+ return {
373
+ "*.exe", "*.dll", "*.so", "*.dylib", "*.a", "*.lib",
374
+ "*.obj", "*.o", "*.png", "*.jpg", "*.jpeg", "*.gif",
375
+ "*.bmp", "*.ico", "*.svg", "*.mp3", "*.mp4", "*.avi",
376
+ "*.mov", "*.wav", "*.pdf", "*.zip", "*.tar", "*.gz",
377
+ "*.7z", "*.rar", "*.bin", "node_modules/*", ".git/*", "build/*",
378
+ "dist/*", ".cache/*", "*.log", "*.tmp", "*.swp", ".DS_Store",
379
+ "Thumbs.db"};
380
+ }
381
+
382
+ bool MultiFileService::matches_pattern(const std::string &file_path,
383
+ const std::string &pattern) {
384
+ // Simple glob-like pattern matching
385
+ // Convert glob pattern to regex
386
+ std::string regex_pattern = pattern;
387
+
388
+ // Escape special regex characters except * and ?
389
+ std::regex special_chars(R"([\.\+\^\$\(\)\[\]\{\}\|\\])");
390
+ regex_pattern = std::regex_replace(regex_pattern, special_chars, R"(\$&)");
391
+
392
+ // Convert glob wildcards to regex
393
+ std::regex glob_star(R"(\\\*)");
394
+ regex_pattern = std::regex_replace(regex_pattern, glob_star, ".*");
395
+
396
+ std::regex glob_question(R"(\\\?)");
397
+ regex_pattern = std::regex_replace(regex_pattern, glob_question, ".");
398
+
399
+ // Add anchors for full match
400
+ regex_pattern = "^" + regex_pattern + "$";
401
+
402
+ try {
403
+ std::regex pattern_regex(regex_pattern, std::regex_constants::icase);
404
+ return std::regex_match(file_path, pattern_regex) ||
405
+ std::regex_match(
406
+ std::filesystem::path(file_path).filename().string(),
407
+ pattern_regex);
408
+ } catch (const std::exception &) {
409
+ // If regex fails, fall back to simple string matching
410
+ return file_path.find(pattern) != std::string::npos;
411
+ }
412
+ }
413
+
414
+ std::vector<std::string>
415
+ MultiFileService::get_git_status_files(const std::string &directory,
416
+ GitStatusFilter filter) {
417
+ std::vector<std::string> files;
418
+
419
+ try {
420
+ std::string command;
421
+ switch (filter) {
422
+ case GitStatusFilter::TRACKED:
423
+ command = "git ls-files";
424
+ break;
425
+ case GitStatusFilter::UNTRACKED:
426
+ command = "git ls-files --others --exclude-standard";
427
+ break;
428
+ case GitStatusFilter::MODIFIED:
429
+ command = "git diff --name-only HEAD";
430
+ break;
431
+ case GitStatusFilter::STAGED:
432
+ command = "git diff --name-only --cached";
433
+ break;
434
+ case GitStatusFilter::UNSTAGED:
435
+ command = "git diff --name-only";
436
+ break;
437
+ default: // GitStatusFilter::ALL
438
+ command = "git ls-files && git ls-files --others --exclude-standard";
439
+ break;
440
+ }
441
+
442
+ // Change to directory and execute git command
443
+ std::string full_command = "cd \"" + directory + "\" && " + command;
444
+ FILE *pipe = Utils::Platform::open_process(full_command, "r");
445
+ if (!pipe)
446
+ return files;
447
+
448
+ char buffer[256];
449
+ std::string result;
450
+ while (fgets(buffer, sizeof(buffer), pipe) != nullptr) {
451
+ result += buffer;
452
+ }
453
+ Utils::Platform::close_process(pipe);
454
+
455
+ // Parse output into file paths
456
+ std::istringstream iss(result);
457
+ std::string line;
458
+ while (std::getline(iss, line)) {
459
+ if (!line.empty()) {
460
+ // Convert to absolute path
461
+ std::string full_path = directory + "/" + line;
462
+ files.push_back(full_path);
463
+ }
464
+ }
465
+
466
+ } catch ([[maybe_unused]] const std::exception &e) {
467
+ // If git commands fail, return empty vector
468
+ }
469
+
470
+ return files;
471
+ }
472
+
473
+ bool MultiFileService::is_text_file(const std::string &file_path) {
474
+ // Check by extension first
475
+ std::string ext = std::filesystem::path(file_path).extension().string();
476
+ std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower);
477
+
478
+ // Common text file extensions
479
+ static const std::set<std::string> text_extensions(
480
+ Utils::Validator::get_text_extensions().begin(),
481
+ Utils::Validator::get_text_extensions().end());
482
+
483
+ if (text_extensions.count(ext)) {
484
+ return true;
485
+ }
486
+
487
+ // Check files without extensions or with unknown extensions
488
+ try {
489
+ std::ifstream file(file_path, std::ios::binary);
490
+ if (!file.is_open())
491
+ return false;
492
+
493
+ // Read first 512 bytes to check for binary content
494
+ char buffer[512];
495
+ file.read(buffer, sizeof(buffer));
496
+ std::streamsize bytes_read = file.gcount();
497
+
498
+ // Check for null bytes (common in binary files)
499
+ for (std::streamsize i = 0; i < bytes_read; ++i) {
500
+ if (buffer[i] == '\0') {
501
+ return false;
502
+ }
503
+ }
504
+
505
+ return true;
506
+
507
+ } catch ([[maybe_unused]] const std::exception &e) {
508
+ return false;
509
+ }
510
+ }
511
+
512
+ std::string
513
+ MultiFileService::get_language_from_extension(const std::string &file_path) {
514
+ std::string ext = std::filesystem::path(file_path).extension().string();
515
+ std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower);
516
+
517
+ // Map extensions to language names
518
+ static std::map<std::string, std::string> ext_to_lang = {
519
+ {".c", "c"},
520
+ {".h", "c"},
521
+ {".cpp", "cpp"},
522
+ {".cc", "cpp"},
523
+ {".cxx", "cpp"},
524
+ {".hpp", "cpp"},
525
+ {".hxx", "cpp"},
526
+ {".py", "python"},
527
+ {".js", "javascript"},
528
+ {".jsx", "javascript"},
529
+ {".ts", "typescript"},
530
+ {".tsx", "typescript"},
531
+ {".java", "java"},
532
+ {".go", "go"},
533
+ {".rs", "rust"},
534
+ {".rb", "ruby"},
535
+ {".php", "php"},
536
+ {".swift", "swift"},
537
+ {".kt", "kotlin"},
538
+ {".scala", "scala"},
539
+ {".clj", "clojure"},
540
+ {".hs", "haskell"},
541
+ {".ml", "ocaml"},
542
+ {".r", "r"},
543
+ {".m", "matlab"},
544
+ {".pl", "perl"},
545
+ {".sh", "shell"},
546
+ {".bash", "shell"},
547
+ {".zsh", "shell"},
548
+ {".fish", "shell"},
549
+ {".ps1", "powershell"},
550
+ {".sql", "sql"},
551
+ {".html", "html"},
552
+ {".htm", "html"},
553
+ {".css", "css"},
554
+ {".xml", "xml"},
555
+ {".json", "json"},
556
+ {".yaml", "yaml"},
557
+ {".yml", "yaml"},
558
+ {".md", "markdown"},
559
+ {".rst", "restructuredtext"},
560
+ {".tex", "latex"},
561
+ {".dockerfile", "docker"},
562
+ {".makefile", "make"},
563
+ {".cmake", "cmake"}};
564
+
565
+ auto it = ext_to_lang.find(ext);
566
+ return (it != ext_to_lang.end()) ? it->second : "unknown";
567
+ }
568
+
569
+ bool MultiFileService::matches_language_filter(
570
+ const std::string &file_path,
571
+ const std::vector<std::string> &language_filters) {
572
+ if (language_filters.empty()) {
573
+ return true; // No filter means include all
574
+ }
575
+
576
+ std::string file_language = get_language_from_extension(file_path);
577
+
578
+ for (const auto &filter : language_filters) {
579
+ std::string filter_lower = filter;
580
+ std::transform(filter_lower.begin(), filter_lower.end(),
581
+ filter_lower.begin(), ::tolower);
582
+
583
+ if (file_language == filter_lower) {
584
+ return true;
585
+ }
586
+ }
587
+
588
+ return false;
589
+ }
590
+
591
+ } // namespace Services