brighterscript 1.0.0-alpha.44 → 1.0.0-alpha.45

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 (198) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/bsconfig.schema.json +6 -1
  3. package/dist/AstValidationSegmenter.js +6 -1
  4. package/dist/AstValidationSegmenter.js.map +1 -1
  5. package/dist/BsConfig.d.ts +4 -0
  6. package/dist/BusyStatusTracker.d.ts +37 -7
  7. package/dist/BusyStatusTracker.js +73 -8
  8. package/dist/BusyStatusTracker.js.map +1 -1
  9. package/dist/Cache.d.ts +0 -4
  10. package/dist/Cache.js +0 -6
  11. package/dist/Cache.js.map +1 -1
  12. package/dist/CrossScopeValidator.d.ts +1 -1
  13. package/dist/CrossScopeValidator.js +4 -4
  14. package/dist/CrossScopeValidator.js.map +1 -1
  15. package/dist/DiagnosticCollection.d.ts +19 -5
  16. package/dist/DiagnosticCollection.js +71 -23
  17. package/dist/DiagnosticCollection.js.map +1 -1
  18. package/dist/DiagnosticFilterer.d.ts +14 -1
  19. package/dist/DiagnosticFilterer.js +130 -12
  20. package/dist/DiagnosticFilterer.js.map +1 -1
  21. package/dist/DiagnosticManager.d.ts +11 -1
  22. package/dist/DiagnosticManager.js +192 -35
  23. package/dist/DiagnosticManager.js.map +1 -1
  24. package/dist/LanguageServer.d.ts +82 -139
  25. package/dist/LanguageServer.js +402 -980
  26. package/dist/LanguageServer.js.map +1 -1
  27. package/dist/Logger.d.ts +9 -4
  28. package/dist/Logger.js +30 -6
  29. package/dist/Logger.js.map +1 -1
  30. package/dist/PluginInterface.d.ts +8 -8
  31. package/dist/PluginInterface.js.map +1 -1
  32. package/dist/Program.d.ts +23 -4
  33. package/dist/Program.js +294 -194
  34. package/dist/Program.js.map +1 -1
  35. package/dist/ProgramBuilder.d.ts +22 -7
  36. package/dist/ProgramBuilder.js +44 -21
  37. package/dist/ProgramBuilder.js.map +1 -1
  38. package/dist/Scope.d.ts +11 -6
  39. package/dist/Scope.js +60 -36
  40. package/dist/Scope.js.map +1 -1
  41. package/dist/SemanticTokenUtils.js +1 -1
  42. package/dist/SemanticTokenUtils.js.map +1 -1
  43. package/dist/astUtils/reflection.d.ts +4 -4
  44. package/dist/astUtils/reflection.js +12 -10
  45. package/dist/astUtils/reflection.js.map +1 -1
  46. package/dist/bscPlugin/BscPlugin.d.ts +3 -3
  47. package/dist/bscPlugin/BscPlugin.js.map +1 -1
  48. package/dist/bscPlugin/CallExpressionInfo.js +4 -2
  49. package/dist/bscPlugin/CallExpressionInfo.js.map +1 -1
  50. package/dist/bscPlugin/completions/CompletionsProcessor.d.ts +1 -1
  51. package/dist/bscPlugin/completions/CompletionsProcessor.js +15 -15
  52. package/dist/bscPlugin/completions/CompletionsProcessor.js.map +1 -1
  53. package/dist/bscPlugin/completions/CompletionsProcessor.spec.js +82 -5
  54. package/dist/bscPlugin/completions/CompletionsProcessor.spec.js.map +1 -1
  55. package/dist/bscPlugin/hover/HoverProcessor.js +3 -0
  56. package/dist/bscPlugin/hover/HoverProcessor.js.map +1 -1
  57. package/dist/bscPlugin/validation/ScopeValidator.d.ts +4 -1
  58. package/dist/bscPlugin/validation/ScopeValidator.js +161 -61
  59. package/dist/bscPlugin/validation/ScopeValidator.js.map +1 -1
  60. package/dist/common/Sequencer.d.ts +53 -0
  61. package/dist/common/Sequencer.js +232 -0
  62. package/dist/common/Sequencer.js.map +1 -0
  63. package/dist/common/Sequencer.spec.d.ts +1 -0
  64. package/dist/common/Sequencer.spec.js +75 -0
  65. package/dist/common/Sequencer.spec.js.map +1 -0
  66. package/dist/deferred.d.ts +2 -0
  67. package/dist/deferred.js +10 -0
  68. package/dist/deferred.js.map +1 -1
  69. package/dist/examples/plugins/removePrint.d.ts +2 -2
  70. package/dist/examples/plugins/removePrint.js.map +1 -1
  71. package/dist/files/BrsFile.d.ts +1 -1
  72. package/dist/files/BrsFile.js +11 -40
  73. package/dist/files/BrsFile.js.map +1 -1
  74. package/dist/files/BrsFile.spec.js +51 -2
  75. package/dist/files/BrsFile.spec.js.map +1 -1
  76. package/dist/files/BscFile.d.ts +1 -0
  77. package/dist/files/LazyFileData.d.ts +1 -0
  78. package/dist/files/XmlFile.spec.js +1 -1
  79. package/dist/files/XmlFile.spec.js.map +1 -1
  80. package/dist/globalCallables.js +186 -186
  81. package/dist/globalCallables.js.map +1 -1
  82. package/dist/interfaces.d.ts +56 -13
  83. package/dist/interfaces.js.map +1 -1
  84. package/dist/lexer/Lexer.js +1 -1
  85. package/dist/lexer/Lexer.js.map +1 -1
  86. package/dist/logging.d.ts +6 -1
  87. package/dist/logging.js +14 -1
  88. package/dist/logging.js.map +1 -1
  89. package/dist/lsp/ActionQueue.d.ts +35 -0
  90. package/dist/lsp/ActionQueue.js +115 -0
  91. package/dist/lsp/ActionQueue.js.map +1 -0
  92. package/dist/lsp/ActionQueue.spec.d.ts +1 -0
  93. package/dist/lsp/ActionQueue.spec.js +80 -0
  94. package/dist/lsp/ActionQueue.spec.js.map +1 -0
  95. package/dist/lsp/DocumentManager.d.ts +63 -0
  96. package/dist/lsp/DocumentManager.js +122 -0
  97. package/dist/lsp/DocumentManager.js.map +1 -0
  98. package/dist/lsp/DocumentManager.spec.d.ts +1 -0
  99. package/dist/lsp/DocumentManager.spec.js +103 -0
  100. package/dist/lsp/DocumentManager.spec.js.map +1 -0
  101. package/dist/lsp/LspProject.d.ts +231 -0
  102. package/dist/lsp/LspProject.js +3 -0
  103. package/dist/lsp/LspProject.js.map +1 -0
  104. package/dist/lsp/PathFilterer.d.ts +75 -0
  105. package/dist/lsp/PathFilterer.js +196 -0
  106. package/dist/lsp/PathFilterer.js.map +1 -0
  107. package/dist/lsp/PathFilterer.spec.d.ts +1 -0
  108. package/dist/lsp/PathFilterer.spec.js +182 -0
  109. package/dist/lsp/PathFilterer.spec.js.map +1 -0
  110. package/dist/lsp/Project.d.ts +167 -0
  111. package/dist/lsp/Project.js +432 -0
  112. package/dist/lsp/Project.js.map +1 -0
  113. package/dist/lsp/Project.spec.d.ts +1 -0
  114. package/dist/lsp/Project.spec.js +220 -0
  115. package/dist/lsp/Project.spec.js.map +1 -0
  116. package/dist/lsp/ProjectManager.d.ts +221 -0
  117. package/dist/lsp/ProjectManager.js +735 -0
  118. package/dist/lsp/ProjectManager.js.map +1 -0
  119. package/dist/lsp/ProjectManager.spec.d.ts +1 -0
  120. package/dist/lsp/ProjectManager.spec.js +757 -0
  121. package/dist/lsp/ProjectManager.spec.js.map +1 -0
  122. package/dist/lsp/ReaderWriterManager.d.ts +21 -0
  123. package/dist/lsp/ReaderWriterManager.js +60 -0
  124. package/dist/lsp/ReaderWriterManager.js.map +1 -0
  125. package/dist/lsp/worker/MessageHandler.d.ts +99 -0
  126. package/dist/lsp/worker/MessageHandler.js +138 -0
  127. package/dist/lsp/worker/MessageHandler.js.map +1 -0
  128. package/dist/lsp/worker/MessageHandler.spec.d.ts +1 -0
  129. package/dist/lsp/worker/MessageHandler.spec.js +64 -0
  130. package/dist/lsp/worker/MessageHandler.spec.js.map +1 -0
  131. package/dist/lsp/worker/WorkerPool.d.ts +38 -0
  132. package/dist/lsp/worker/WorkerPool.js +78 -0
  133. package/dist/lsp/worker/WorkerPool.js.map +1 -0
  134. package/dist/lsp/worker/WorkerPool.spec.d.ts +1 -0
  135. package/dist/lsp/worker/WorkerPool.spec.js +59 -0
  136. package/dist/lsp/worker/WorkerPool.spec.js.map +1 -0
  137. package/dist/lsp/worker/WorkerThreadProject.d.ts +144 -0
  138. package/dist/lsp/worker/WorkerThreadProject.js +183 -0
  139. package/dist/lsp/worker/WorkerThreadProject.js.map +1 -0
  140. package/dist/lsp/worker/WorkerThreadProject.spec.d.ts +2 -0
  141. package/dist/lsp/worker/WorkerThreadProject.spec.js +68 -0
  142. package/dist/lsp/worker/WorkerThreadProject.spec.js.map +1 -0
  143. package/dist/lsp/worker/WorkerThreadProjectRunner.d.ts +15 -0
  144. package/dist/lsp/worker/WorkerThreadProjectRunner.js +58 -0
  145. package/dist/lsp/worker/WorkerThreadProjectRunner.js.map +1 -0
  146. package/dist/parser/Expression.js +7 -2
  147. package/dist/parser/Expression.js.map +1 -1
  148. package/dist/parser/Parser.spec.js +12 -0
  149. package/dist/parser/Parser.spec.js.map +1 -1
  150. package/dist/parser/Statement.js +2 -2
  151. package/dist/parser/Statement.js.map +1 -1
  152. package/dist/parser/tests/expression/TemplateStringExpression.spec.js +51 -5
  153. package/dist/parser/tests/expression/TemplateStringExpression.spec.js.map +1 -1
  154. package/dist/roku-types/data.json +745 -751
  155. package/dist/types/BooleanType.d.ts +0 -2
  156. package/dist/types/BooleanType.js +4 -6
  157. package/dist/types/BooleanType.js.map +1 -1
  158. package/dist/types/BscType.js +5 -0
  159. package/dist/types/BscType.js.map +1 -1
  160. package/dist/types/BuiltInInterfaceAdder.d.ts +1 -0
  161. package/dist/types/BuiltInInterfaceAdder.js +24 -17
  162. package/dist/types/BuiltInInterfaceAdder.js.map +1 -1
  163. package/dist/types/DoubleType.d.ts +0 -2
  164. package/dist/types/DoubleType.js +4 -6
  165. package/dist/types/DoubleType.js.map +1 -1
  166. package/dist/types/DynamicType.d.ts +0 -2
  167. package/dist/types/DynamicType.js +3 -5
  168. package/dist/types/DynamicType.js.map +1 -1
  169. package/dist/types/FloatType.d.ts +0 -2
  170. package/dist/types/FloatType.js +4 -6
  171. package/dist/types/FloatType.js.map +1 -1
  172. package/dist/types/FunctionType.d.ts +0 -2
  173. package/dist/types/FunctionType.js +5 -7
  174. package/dist/types/FunctionType.js.map +1 -1
  175. package/dist/types/IntegerType.d.ts +0 -2
  176. package/dist/types/IntegerType.js +4 -6
  177. package/dist/types/IntegerType.js.map +1 -1
  178. package/dist/types/LongIntegerType.d.ts +0 -2
  179. package/dist/types/LongIntegerType.js +4 -6
  180. package/dist/types/LongIntegerType.js.map +1 -1
  181. package/dist/types/ObjectType.d.ts +3 -3
  182. package/dist/types/ObjectType.js +6 -8
  183. package/dist/types/ObjectType.js.map +1 -1
  184. package/dist/types/StringType.d.ts +0 -2
  185. package/dist/types/StringType.js +4 -6
  186. package/dist/types/StringType.js.map +1 -1
  187. package/dist/types/VoidType.d.ts +0 -2
  188. package/dist/types/VoidType.js +4 -6
  189. package/dist/types/VoidType.js.map +1 -1
  190. package/dist/types/helpers.d.ts +4 -0
  191. package/dist/types/helpers.js +5 -1
  192. package/dist/types/helpers.js.map +1 -1
  193. package/dist/util.d.ts +44 -16
  194. package/dist/util.js +196 -70
  195. package/dist/util.js.map +1 -1
  196. package/dist/validators/ClassValidator.js +2 -2
  197. package/dist/validators/ClassValidator.js.map +1 -1
  198. package/package.json +15 -5
@@ -0,0 +1,735 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.ProjectManager = void 0;
10
+ const util_1 = require("../util");
11
+ const roku_deploy_1 = require("roku-deploy");
12
+ const path = require("path");
13
+ const EventEmitter = require("eventemitter3");
14
+ const Project_1 = require("./Project");
15
+ const WorkerThreadProject_1 = require("./worker/WorkerThreadProject");
16
+ const vscode_languageserver_protocol_1 = require("vscode-languageserver-protocol");
17
+ const deferred_1 = require("../deferred");
18
+ const DocumentManager_1 = require("./DocumentManager");
19
+ const BusyStatusTracker_1 = require("../BusyStatusTracker");
20
+ const fastGlob = require("fast-glob");
21
+ const PathFilterer_1 = require("./PathFilterer");
22
+ const logging_1 = require("../logging");
23
+ const Cache_1 = require("../Cache");
24
+ const ActionQueue_1 = require("./ActionQueue");
25
+ const fsExtra = require("fs-extra");
26
+ const FileChangeTypeLookup = Object.entries(vscode_languageserver_protocol_1.FileChangeType).reduce((acc, [key, value]) => {
27
+ acc[value] = key;
28
+ acc[key] = value;
29
+ return acc;
30
+ }, {});
31
+ /**
32
+ * Manages all brighterscript projects for the language server
33
+ */
34
+ class ProjectManager {
35
+ constructor(options) {
36
+ var _a, _b;
37
+ /**
38
+ * Collection of all projects
39
+ */
40
+ this.projects = [];
41
+ /**
42
+ * Collection of standalone projects. These are projects that are not part of a workspace, but are instead single files.
43
+ * All of these are also present in the `projects` collection.
44
+ */
45
+ this.standaloneProjects = new Map();
46
+ this.busyStatusTracker = new BusyStatusTracker_1.BusyStatusTracker();
47
+ this.firstSync = new deferred_1.Deferred();
48
+ this.fileChangesQueue = new ActionQueue_1.ActionQueue({
49
+ maxActionDuration: 45000
50
+ });
51
+ this.emitter = new EventEmitter();
52
+ this.logger = (_a = options === null || options === void 0 ? void 0 : options.logger) !== null && _a !== void 0 ? _a : (0, logging_1.createLogger)();
53
+ this.pathFilterer = (_b = options === null || options === void 0 ? void 0 : options.pathFilterer) !== null && _b !== void 0 ? _b : new PathFilterer_1.PathFilterer({ logger: options === null || options === void 0 ? void 0 : options.logger });
54
+ this.documentManager = new DocumentManager_1.DocumentManager({
55
+ delay: ProjectManager.documentManagerDelay,
56
+ flushHandler: (event) => {
57
+ return this.flushDocumentChanges(event).catch(e => console.error(e));
58
+ }
59
+ });
60
+ this.on('validate-begin', (event) => {
61
+ this.busyStatusTracker.beginScopedRun(event.project, `validate-project`);
62
+ });
63
+ this.on('validate-end', (event) => {
64
+ void this.busyStatusTracker.endScopedRun(event.project, `validate-project`);
65
+ });
66
+ }
67
+ /**
68
+ * Apply all of the queued document changes. This should only be called as a result of the documentManager flushing changes, and never called manually
69
+ * @param event the document changes that have occurred since the last time we applied
70
+ */
71
+ async flushDocumentChanges(event) {
72
+ var _a;
73
+ this.logger.info(`flushDocumentChanges`, (_a = event === null || event === void 0 ? void 0 : event.actions) === null || _a === void 0 ? void 0 : _a.map(x => ({
74
+ type: x.type,
75
+ srcPath: x.srcPath,
76
+ allowStandaloneProject: x.allowStandaloneProject
77
+ })));
78
+ //ensure that we're fully initialized before proceeding
79
+ await this.onInitialized();
80
+ const actions = [...event.actions];
81
+ let idSequence = 0;
82
+ //add an ID to every action (so we can track which actions were handled by which projects)
83
+ for (const action of actions) {
84
+ action.id = idSequence++;
85
+ }
86
+ //apply all of the document actions to each project in parallel
87
+ const responses = await Promise.all(this.projects.map(async (project) => {
88
+ //wait for this project to finish activating
89
+ await project.whenActivated();
90
+ const filterer = new PathFilterer_1.PathCollection({
91
+ rootDir: project.rootDir,
92
+ globs: project.filePatterns
93
+ });
94
+ // only include files that are applicable to this specific project (still allow deletes to flow through since they're cheap)
95
+ const projectActions = actions.filter(action => {
96
+ return (
97
+ //if this is a delete, just pass it through because they're cheap to apply
98
+ action.type === 'delete' ||
99
+ //if this is a set, only pass it through if it's a file that this project cares about
100
+ filterer.isMatch(action.srcPath));
101
+ });
102
+ if (projectActions.length > 0) {
103
+ const responseActions = await project.applyFileChanges(projectActions);
104
+ return responseActions.map(x => ({
105
+ project: project,
106
+ action: x
107
+ }));
108
+ }
109
+ }));
110
+ //create standalone projects for any files not handled by any project
111
+ const flatResponses = responses.flat();
112
+ for (const action of actions) {
113
+ //skip this action if it doesn't support standalone projects
114
+ if (!action.allowStandaloneProject || action.type !== 'set') {
115
+ continue;
116
+ }
117
+ //a list of responses that handled this action
118
+ const handledResponses = flatResponses.filter(x => { var _a, _b; return ((_a = x === null || x === void 0 ? void 0 : x.action) === null || _a === void 0 ? void 0 : _a.id) === action.id && ((_b = x === null || x === void 0 ? void 0 : x.action) === null || _b === void 0 ? void 0 : _b.status) === 'accepted'; });
119
+ //remove any standalone project created for this file since it was handled by a normal project
120
+ const normalProjectsThatHandledThisFile = handledResponses.filter(x => !x.project.isStandaloneProject);
121
+ if (normalProjectsThatHandledThisFile.length > 0) {
122
+ //if there's a standalone project for this file, delete it
123
+ if (this.getStandaloneProject(action.srcPath, false)) {
124
+ this.logger.debug(`flushDocumentChanges: removing standalone project because the following normal projects handled the file: '${action.srcPath}', projects:`, normalProjectsThatHandledThisFile.map(x => x.project.projectIdentifier));
125
+ this.removeStandaloneProject(action.srcPath);
126
+ }
127
+ // create a standalone project if this action was handled by zero normal projects.
128
+ //(safe to call even if there's already a standalone project, won't create dupes)
129
+ }
130
+ else {
131
+ //TODO only create standalone projects for files we understand (brightscript, brighterscript, scenegraph xml, etc)
132
+ await this.createStandaloneProject(action.srcPath);
133
+ }
134
+ }
135
+ this.logger.info('flushDocumentChanges complete', actions.map(x => ({
136
+ type: x.type,
137
+ srcPath: x.srcPath,
138
+ allowStandaloneProject: x.allowStandaloneProject
139
+ })));
140
+ }
141
+ /**
142
+ * Get a standalone project for a given file path
143
+ */
144
+ getStandaloneProject(srcPath, standardizePath = true) {
145
+ return this.standaloneProjects.get(standardizePath ? util_1.util.standardizePath(srcPath) : srcPath);
146
+ }
147
+ /**
148
+ * Create a project that validates a single file. This is useful for getting language support for files that don't belong to a project
149
+ */
150
+ async createStandaloneProject(srcPath) {
151
+ srcPath = util_1.util.standardizePath(srcPath);
152
+ //if we already have a standalone project with this path, do nothing because it already exists
153
+ if (this.getStandaloneProject(srcPath, false)) {
154
+ this.logger.log('createStandaloneProject skipping because we already have one for this path');
155
+ return;
156
+ }
157
+ this.logger.log(`Creating standalone project for '${srcPath}'`);
158
+ const projectNumber = ProjectManager.projectNumberSequence++;
159
+ const rootDir = path.join(__dirname, `standalone-project-${projectNumber}`);
160
+ const projectOptions = {
161
+ //these folders don't matter for standalone projects
162
+ workspaceFolder: rootDir,
163
+ projectPath: rootDir,
164
+ enableThreading: false,
165
+ projectNumber: projectNumber,
166
+ files: [{
167
+ src: srcPath,
168
+ dest: 'source/standalone.brs'
169
+ }]
170
+ };
171
+ const project = this.constructProject(projectOptions);
172
+ project.srcPath = srcPath;
173
+ project.isStandaloneProject = true;
174
+ this.standaloneProjects.set(srcPath, project);
175
+ await this.activateProject(project, projectOptions);
176
+ }
177
+ removeStandaloneProject(srcPath) {
178
+ srcPath = util_1.util.standardizePath(srcPath);
179
+ const project = this.getStandaloneProject(srcPath, false);
180
+ if (project) {
181
+ if (project.srcPath === srcPath) {
182
+ this.logger.debug(`Removing standalone project for file '${srcPath}'`);
183
+ this.removeProject(project);
184
+ this.standaloneProjects.delete(srcPath);
185
+ }
186
+ }
187
+ }
188
+ /**
189
+ * Get a promise that resolves when this manager is finished initializing
190
+ */
191
+ onInitialized() {
192
+ return Promise.allSettled([
193
+ //wait for the first sync to finish
194
+ this.firstSync.promise,
195
+ //make sure we're not in the middle of a sync
196
+ this.syncPromise,
197
+ //make sure all projects are activated
198
+ ...this.projects.map(x => x.whenActivated())
199
+ ]);
200
+ }
201
+ /**
202
+ * Get a promise that resolves when the project manager is idle (no pending work)
203
+ */
204
+ async onIdle() {
205
+ await this.onInitialized();
206
+ //There are race conditions where the fileChangesQueue will become idle, but that causes the documentManager
207
+ //to start a new flush. So we must keep waiting until everything is idle
208
+ while (!this.documentManager.isIdle || !this.fileChangesQueue.isIdle) {
209
+ this.logger.debug('onIdle', { documentManagerIdle: this.documentManager.isIdle, fileChangesQueueIdle: this.fileChangesQueue.isIdle });
210
+ await Promise.allSettled([
211
+ //make sure all pending file changes have been flushed
212
+ this.documentManager.onIdle(),
213
+ //wait for the file changes queue to be idle
214
+ this.fileChangesQueue.onIdle()
215
+ ]);
216
+ }
217
+ this.logger.info('onIdle debug', { documentManagerIdle: this.documentManager.isIdle, fileChangesQueueIdle: this.fileChangesQueue.isIdle });
218
+ }
219
+ /**
220
+ * Given a list of all desired projects, create any missing projects and destroy and projects that are no longer available
221
+ * Treat workspaces that don't have a bsconfig.json as a project.
222
+ * Handle situations where bsconfig.json files were added or removed (to elevate/lower workspaceFolder projects accordingly)
223
+ * Leave existing projects alone if they are not affected by these changes
224
+ * @param workspaceConfigs an array of workspaces
225
+ */
226
+ async syncProjects(workspaceConfigs, forceReload = false) {
227
+ //if we're force reloading, destroy all projects and start fresh
228
+ if (forceReload) {
229
+ this.logger.log('syncProjects: forceReload is true so removing all existing projects');
230
+ for (const project of this.projects) {
231
+ this.removeProject(project);
232
+ }
233
+ }
234
+ this.logger.log('syncProjects', workspaceConfigs.map(x => x.workspaceFolder));
235
+ this.syncPromise = (async () => {
236
+ //build a list of unique projects across all workspace folders
237
+ let projectConfigs = (await Promise.all(workspaceConfigs.map(async (workspaceConfig) => {
238
+ const projectPaths = await this.getProjectPaths(workspaceConfig);
239
+ return projectPaths.map(projectPath => ({
240
+ projectPath: (0, util_1.standardizePath) `${projectPath}`,
241
+ workspaceFolder: (0, util_1.standardizePath) `${workspaceConfig.workspaceFolder}`,
242
+ excludePatterns: workspaceConfig.excludePatterns,
243
+ enableThreading: workspaceConfig.enableThreading
244
+ }));
245
+ }))).flat(1);
246
+ //filter the project paths to only include those that are allowed by the path filterer
247
+ projectConfigs = this.pathFilterer.filter(projectConfigs, x => x.projectPath);
248
+ //delete projects not represented in the list
249
+ for (const project of this.projects) {
250
+ //we can't find this existing project in our new list, so scrap it
251
+ if (!projectConfigs.find(x => x.projectPath === project.projectPath)) {
252
+ this.removeProject(project);
253
+ }
254
+ }
255
+ // skip projects we already have (they're already loaded...no need to reload them)
256
+ projectConfigs = projectConfigs.filter(x => {
257
+ return !this.hasProject(x.projectPath);
258
+ });
259
+ //dedupe by project path
260
+ projectConfigs = [
261
+ ...projectConfigs.reduce((acc, x) => acc.set(x.projectPath, x), new Map()).values()
262
+ ];
263
+ //create missing projects
264
+ await Promise.all(projectConfigs.map(async (config) => {
265
+ await this.createAndActivateProject(config);
266
+ }));
267
+ //mark that we've completed our first sync
268
+ this.firstSync.tryResolve();
269
+ })();
270
+ //return the sync promise
271
+ return this.syncPromise;
272
+ }
273
+ handleFileChanges(changes) {
274
+ this.logger.debug('handleFileChanges', changes.map(x => `${FileChangeTypeLookup[x.type]}: ${x.srcPath}`));
275
+ //this function should NOT be marked as async, because typescript wraps the body in an async call sometimes. These need to be registered synchronously
276
+ return this.fileChangesQueue.run(async (changes) => {
277
+ //wait for any pending syncs to finish
278
+ await this.onInitialized();
279
+ return this._handleFileChanges(changes);
280
+ }, changes);
281
+ }
282
+ /**
283
+ * Handle when files or directories are added, changed, or deleted in the workspace.
284
+ * This is safe to call any time. Changes will be queued and flushed at the correct times
285
+ */
286
+ async _handleFileChanges(changes) {
287
+ //normalize srcPath for all changes
288
+ for (const change of changes) {
289
+ change.srcPath = util_1.util.standardizePath(change.srcPath);
290
+ }
291
+ //filter any changes that are not allowed by the path filterer
292
+ changes = this.pathFilterer.filter(changes, x => x.srcPath);
293
+ this.logger.debug('handleFileChanges -> filtered', changes.map(x => `${FileChangeTypeLookup[x.type]}: ${x.srcPath}`));
294
+ //process all file changes in parallel
295
+ await Promise.all(changes.map(async (change) => {
296
+ await this.handleFileChange(change);
297
+ }));
298
+ }
299
+ /**
300
+ * Handle a single file change. If the file is a directory, this will recursively read all files in the directory and call `handleFileChanges` again
301
+ */
302
+ async handleFileChange(change) {
303
+ if (change.type === vscode_languageserver_protocol_1.FileChangeType.Deleted) {
304
+ //mark this document or directory as deleted
305
+ this.documentManager.delete(change.srcPath);
306
+ //file added or changed
307
+ }
308
+ else {
309
+ //if this is a new directory, read all files recursively and register those as file changes too
310
+ if (util_1.util.isDirectorySync(change.srcPath)) {
311
+ const files = await fastGlob('**/*', {
312
+ cwd: change.srcPath,
313
+ onlyFiles: true,
314
+ absolute: true
315
+ });
316
+ //pipe all files found recursively in the new directory through this same function so they can be processed correctly
317
+ await Promise.all(files.map((srcPath) => {
318
+ return this.handleFileChange({
319
+ srcPath: util_1.util.standardizePath(srcPath),
320
+ type: vscode_languageserver_protocol_1.FileChangeType.Changed,
321
+ allowStandaloneProject: change.allowStandaloneProject
322
+ });
323
+ }));
324
+ //this is a new file. set the file contents
325
+ }
326
+ else {
327
+ this.documentManager.set({
328
+ srcPath: change.srcPath,
329
+ fileContents: change.fileContents,
330
+ allowStandaloneProject: change.allowStandaloneProject
331
+ });
332
+ }
333
+ }
334
+ //reload any projects whose bsconfig.json was changed
335
+ const projectsToReload = this.projects.filter(project => {
336
+ var _a;
337
+ //this is a path to a bsconfig.json file
338
+ if (((_a = project.bsconfigPath) === null || _a === void 0 ? void 0 : _a.toLowerCase()) === change.srcPath.toLowerCase()) {
339
+ //fetch file contents if we don't already have them
340
+ if (!change.fileContents) {
341
+ try {
342
+ change.fileContents = fsExtra.readFileSync(project.bsconfigPath).toString();
343
+ }
344
+ finally { }
345
+ }
346
+ ///the bsconfig contents have changed since we last saw it, so reload this project
347
+ if (project.bsconfigFileContents !== change.fileContents) {
348
+ return true;
349
+ }
350
+ }
351
+ return false;
352
+ });
353
+ if (projectsToReload.length > 0) {
354
+ await Promise.all(projectsToReload.map(x => this.reloadProject(x)));
355
+ }
356
+ }
357
+ /**
358
+ * Handle when a file is closed in the editor (this mostly just handles removing standalone projects)
359
+ */
360
+ async handleFileClose(event) {
361
+ this.logger.debug(`File was closed. ${event.srcPath}`);
362
+ this.removeStandaloneProject(event.srcPath);
363
+ //most other methods on this class are async, might as well make this one async too for consistency and future expansion
364
+ await Promise.resolve();
365
+ }
366
+ /**
367
+ * Given a project, forcibly reload it by removing it and re-adding it
368
+ */
369
+ async reloadProject(project) {
370
+ this.logger.log('Reloading project', { projectPath: project.projectPath });
371
+ this.removeProject(project);
372
+ project = await this.createAndActivateProject(project.activateOptions);
373
+ }
374
+ /**
375
+ * Get all the semantic tokens for the given file
376
+ * @returns an array of semantic tokens
377
+ */
378
+ async getSemanticTokens(options) {
379
+ //wait for all pending syncs to finish
380
+ await this.onIdle();
381
+ let result = await util_1.util.promiseRaceMatch(this.projects.map(x => x.getSemanticTokens(options)),
382
+ //keep the first non-falsey result
383
+ (result) => (result === null || result === void 0 ? void 0 : result.length) > 0);
384
+ return result;
385
+ }
386
+ /**
387
+ * Get a string containing the transpiled contents of the file at the given path
388
+ * @returns the transpiled contents of the file as a string
389
+ */
390
+ async transpileFile(options) {
391
+ //wait for all pending syncs to finish
392
+ await this.onIdle();
393
+ let result = await util_1.util.promiseRaceMatch(this.projects.map(x => x.transpileFile(options)),
394
+ //keep the first non-falsey result
395
+ (result) => !!result);
396
+ return result;
397
+ }
398
+ /**
399
+ * Get the completions for the given position in the file
400
+ */
401
+ async getCompletions(options) {
402
+ var _a;
403
+ await this.onIdle();
404
+ //if the request has been cancelled since originally requested due to idle time being slow, skip the rest of the wor
405
+ if ((_a = options === null || options === void 0 ? void 0 : options.cancellationToken) === null || _a === void 0 ? void 0 : _a.isCancellationRequested) {
406
+ this.logger.log('ProjectManager getCompletions cancelled', options);
407
+ return;
408
+ }
409
+ this.logger.log('ProjectManager getCompletions', options);
410
+ //Ask every project for results, keep whichever one responds first that has a valid response
411
+ let result = await util_1.util.promiseRaceMatch(this.projects.map(x => x.getCompletions(options)),
412
+ //keep the first non-falsey result
413
+ (result) => { var _a; return ((_a = result === null || result === void 0 ? void 0 : result.items) === null || _a === void 0 ? void 0 : _a.length) > 0; });
414
+ return result;
415
+ }
416
+ /**
417
+ * Get the hover information for the given position in the file. If multiple projects have hover information, the projects will be raced and
418
+ * the fastest result will be returned
419
+ * @returns the hover information or undefined if no hover information was found
420
+ */
421
+ async getHover(options) {
422
+ //wait for all pending syncs to finish
423
+ await this.onIdle();
424
+ //Ask every project for hover info, keep whichever one responds first that has a valid response
425
+ let hover = await util_1.util.promiseRaceMatch(this.projects.map(x => x.getHover(options)),
426
+ //keep the first set of non-empty results
427
+ (result) => (result === null || result === void 0 ? void 0 : result.length) > 0);
428
+ return hover === null || hover === void 0 ? void 0 : hover[0];
429
+ }
430
+ /**
431
+ * Get the definition for the symbol at the given position in the file
432
+ * @returns a list of locations where the symbol under the position is defined in the project
433
+ */
434
+ async getDefinition(options) {
435
+ //wait for all pending syncs to finish
436
+ await this.onIdle();
437
+ //TODO should we merge definitions across ALL projects? or just return definitions from the first project we found
438
+ //Ask every project for definition info, keep whichever one responds first that has a valid response
439
+ let result = await util_1.util.promiseRaceMatch(this.projects.map(x => x.getDefinition(options)),
440
+ //keep the first non-falsey result
441
+ (result) => !!result);
442
+ return result;
443
+ }
444
+ async getSignatureHelp(options) {
445
+ var _a;
446
+ //wait for all pending syncs to finish
447
+ await this.onIdle();
448
+ //Ask every project for definition info, keep whichever one responds first that has a valid response
449
+ let signatures = await util_1.util.promiseRaceMatch(this.projects.map(x => x.getSignatureHelp(options)),
450
+ //keep the first non-falsey result
451
+ (result) => !!result);
452
+ if ((signatures === null || signatures === void 0 ? void 0 : signatures.length) > 0) {
453
+ const activeSignature = signatures.length > 0 ? 0 : undefined;
454
+ const activeParameter = activeSignature >= 0 ? (_a = signatures[activeSignature]) === null || _a === void 0 ? void 0 : _a.index : undefined;
455
+ let result = {
456
+ signatures: signatures.map((s) => s.signature),
457
+ activeSignature: activeSignature,
458
+ activeParameter: activeParameter
459
+ };
460
+ return result;
461
+ }
462
+ }
463
+ async getDocumentSymbol(options) {
464
+ //wait for all pending syncs to finish
465
+ await this.onIdle();
466
+ //Ask every project for definition info, keep whichever one responds first that has a valid response
467
+ let result = await util_1.util.promiseRaceMatch(this.projects.map(x => x.getDocumentSymbol(options)),
468
+ //keep the first non-falsey result
469
+ (result) => !!result);
470
+ return result;
471
+ }
472
+ async getWorkspaceSymbol() {
473
+ //wait for all pending syncs to finish
474
+ await this.onIdle();
475
+ //Ask every project for definition info, keep whichever one responds first that has a valid response
476
+ let responses = await Promise.allSettled(this.projects.map(x => x.getWorkspaceSymbol()));
477
+ let results = responses
478
+ //keep all symbol results
479
+ .map((x) => {
480
+ return x.status === 'fulfilled' ? x.value : [];
481
+ })
482
+ //flatten the array
483
+ .flat()
484
+ //throw out nulls
485
+ .filter(x => !!x);
486
+ // Remove duplicates
487
+ const allSymbols = Object.values(results.reduce((map, symbol) => {
488
+ const key = symbol.location.uri + symbol.name;
489
+ map[key] = symbol;
490
+ return map;
491
+ }, {}));
492
+ return allSymbols;
493
+ }
494
+ async getReferences(options) {
495
+ //wait for all pending syncs to finish
496
+ await this.onIdle();
497
+ //Ask every project for definition info, keep whichever one responds first that has a valid response
498
+ let result = await util_1.util.promiseRaceMatch(this.projects.map(x => x.getReferences(options)),
499
+ //keep the first non-falsey result
500
+ (result) => !!result);
501
+ return result !== null && result !== void 0 ? result : [];
502
+ }
503
+ async getCodeActions(options) {
504
+ //wait for all pending syncs to finish
505
+ await this.onIdle();
506
+ //Ask every project for definition info, keep whichever one responds first that has a valid response
507
+ let result = await util_1.util.promiseRaceMatch(this.projects.map(x => x.getCodeActions(options)),
508
+ //keep the first non-falsey result
509
+ (result) => !!result);
510
+ return result;
511
+ }
512
+ /**
513
+ * Scan a given workspace for all `bsconfig.json` files. If at least one is found, then only folders who have bsconfig.json are returned.
514
+ * If none are found, then the workspaceFolder itself is treated as a project
515
+ */
516
+ async getProjectPaths(workspaceConfig) {
517
+ var _a;
518
+ //get the list of exclude patterns, and negate them (so they actually work like excludes)
519
+ const excludePatterns = ((_a = workspaceConfig.excludePatterns) !== null && _a !== void 0 ? _a : []).map(x => (0, util_1.standardizePath) `!${x}`);
520
+ let files = await roku_deploy_1.rokuDeploy.getFilePaths([
521
+ '**/bsconfig.json',
522
+ //exclude all files found in `files.exclude`
523
+ ...excludePatterns
524
+ ], workspaceConfig.workspaceFolder);
525
+ //filter the files to only include those that are allowed by the path filterer
526
+ files = this.pathFilterer.filter(files, x => x.src);
527
+ //if we found at least one bsconfig.json, then ALL projects must have a bsconfig.json.
528
+ if (files.length > 0) {
529
+ return files.map(file => (0, util_1.standardizePath) `${path.dirname(file.src)}`);
530
+ }
531
+ //look for roku project folders
532
+ let rokuLikeDirs = (await Promise.all(
533
+ //find all folders containing a `manifest` file
534
+ (await roku_deploy_1.rokuDeploy.getFilePaths([
535
+ '**/manifest',
536
+ ...excludePatterns
537
+ //is there at least one .bs|.brs file under the `/source` folder?
538
+ ], workspaceConfig.workspaceFolder)).map(async (manifestEntry) => {
539
+ const manifestDir = path.dirname(manifestEntry.src);
540
+ const files = await roku_deploy_1.rokuDeploy.getFilePaths([
541
+ 'source/**/*.{brs,bs}',
542
+ ...excludePatterns
543
+ ], manifestDir);
544
+ if (files.length > 0) {
545
+ return manifestDir;
546
+ }
547
+ })
548
+ //throw out nulls
549
+ )).filter(x => !!x);
550
+ //throw out any directories that are not allowed by the path filterer
551
+ rokuLikeDirs = this.pathFilterer.filter(rokuLikeDirs, srcPath => srcPath);
552
+ if (rokuLikeDirs.length > 0) {
553
+ return rokuLikeDirs;
554
+ }
555
+ //treat the workspace folder as a brightscript project itself
556
+ return [workspaceConfig.workspaceFolder];
557
+ }
558
+ /**
559
+ * Returns true if we have this project, or false if we don't
560
+ * @param projectPath path to the project
561
+ * @returns true if the project exists, or false if it doesn't
562
+ */
563
+ hasProject(projectPath) {
564
+ return !!this.getProject(projectPath);
565
+ }
566
+ /**
567
+ * Get a project with the specified path
568
+ * @param param path to the project or an obj that has `projectPath` prop
569
+ * @returns a project, or undefined if no project was found
570
+ */
571
+ getProject(param) {
572
+ const projectPath = util_1.util.standardizePath((typeof param === 'string') ? param : param.projectPath);
573
+ return this.projects.find(x => x.projectPath === projectPath);
574
+ }
575
+ /**
576
+ * Remove a project from the language server
577
+ */
578
+ removeProject(project) {
579
+ const idx = this.projects.findIndex(x => x.projectPath === (project === null || project === void 0 ? void 0 : project.projectPath));
580
+ if (idx > -1) {
581
+ this.logger.log('Removing project', { projectPath: project.projectPath, projectNumber: project.projectNumber });
582
+ this.projects.splice(idx, 1);
583
+ }
584
+ //anytime we remove a project, we should emit an event that clears all of its diagnostics
585
+ this.emit('diagnostics', { project: project, diagnostics: [] });
586
+ project === null || project === void 0 ? void 0 : project.dispose();
587
+ this.busyStatusTracker.endAllRunsForScope(project);
588
+ }
589
+ /**
590
+ * Get a projectNumber for a given config. Try to reuse project numbers when we've seen this project before
591
+ * - If the config already has one, use that.
592
+ * - If we've already seen this config before, use the same project number as before
593
+ */
594
+ getProjectNumber(config) {
595
+ if (config.projectNumber !== undefined) {
596
+ return config.projectNumber;
597
+ }
598
+ return ProjectManager.projectNumberCache.getOrAdd(`${(0, util_1.standardizePath)(config.projectPath)}-${(0, util_1.standardizePath)(config.workspaceFolder)}-${config.bsconfigPath}`, () => {
599
+ return ProjectManager.projectNumberSequence++;
600
+ });
601
+ }
602
+ /**
603
+ * Constructs a project for the given config. Just makes the project, doesn't activate it
604
+ * @returns a new project, or the existing project if one already exists with this config info
605
+ */
606
+ constructProject(config) {
607
+ //skip this project if we already have it
608
+ if (this.hasProject(config.projectPath)) {
609
+ return this.getProject(config.projectPath);
610
+ }
611
+ config.projectNumber = this.getProjectNumber(config);
612
+ const projectIdentifier = `prj${config.projectNumber}`;
613
+ let project = config.enableThreading
614
+ ? new WorkerThreadProject_1.WorkerThreadProject({
615
+ logger: this.logger.createLogger(),
616
+ projectIdentifier: projectIdentifier
617
+ })
618
+ : new Project_1.Project({
619
+ logger: this.logger.createLogger(),
620
+ projectIdentifier: projectIdentifier
621
+ });
622
+ this.logger.log(`Created project #${config.projectNumber} for: "${config.projectPath}" (${config.enableThreading ? 'worker thread' : 'main thread'})`);
623
+ this.projects.push(project);
624
+ //pipe all project-specific events through our emitter, and include the project reference
625
+ project.on('all', (eventName, data) => {
626
+ this.emit(eventName, Object.assign(Object.assign({}, data), { project: project }));
627
+ });
628
+ return project;
629
+ }
630
+ /**
631
+ * Constructs a project for the given config
632
+ * @returns a new project, or the existing project if one already exists with this config info
633
+ */
634
+ async createAndActivateProject(config) {
635
+ //skip this project if we already have it
636
+ if (this.hasProject(config.projectPath)) {
637
+ return this.getProject(config.projectPath);
638
+ }
639
+ const project = this.constructProject(config);
640
+ await this.activateProject(project, config);
641
+ return project;
642
+ }
643
+ async activateProject(project, config) {
644
+ this.logger.debug('Activating project', project.projectIdentifier, {
645
+ projectPath: config === null || config === void 0 ? void 0 : config.projectPath,
646
+ bsconfigPath: config.bsconfigPath
647
+ });
648
+ await project.activate(config);
649
+ //send an event to indicate that this project has been activated
650
+ this.emit('project-activate', { project: project });
651
+ //register this project's list of files with the path filterer
652
+ const unregister = this.pathFilterer.registerIncludeList(project.rootDir, project.filePatterns);
653
+ project.disposables.push({ dispose: unregister });
654
+ }
655
+ on(eventName, handler) {
656
+ this.emitter.on(eventName, handler);
657
+ return () => {
658
+ this.emitter.removeListener(eventName, handler);
659
+ };
660
+ }
661
+ async emit(eventName, data) {
662
+ //emit these events on next tick, otherwise they will be processed immediately which could cause issues
663
+ await util_1.util.sleep(0);
664
+ this.emitter.emit(eventName, data);
665
+ }
666
+ dispose() {
667
+ var _a;
668
+ this.emitter.removeAllListeners();
669
+ for (const project of this.projects) {
670
+ (_a = project === null || project === void 0 ? void 0 : project.dispose) === null || _a === void 0 ? void 0 : _a.call(project);
671
+ }
672
+ }
673
+ }
674
+ ProjectManager.documentManagerDelay = 150;
675
+ /**
676
+ * A unique project counter to help distinguish log entries in lsp mode
677
+ */
678
+ ProjectManager.projectNumberSequence = 0;
679
+ ProjectManager.projectNumberCache = new Cache_1.Cache();
680
+ __decorate([
681
+ TrackBusyStatus
682
+ ], ProjectManager.prototype, "flushDocumentChanges", null);
683
+ __decorate([
684
+ TrackBusyStatus
685
+ ], ProjectManager.prototype, "syncProjects", null);
686
+ __decorate([
687
+ TrackBusyStatus
688
+ ], ProjectManager.prototype, "getSemanticTokens", null);
689
+ __decorate([
690
+ TrackBusyStatus
691
+ ], ProjectManager.prototype, "transpileFile", null);
692
+ __decorate([
693
+ TrackBusyStatus
694
+ ], ProjectManager.prototype, "getCompletions", null);
695
+ __decorate([
696
+ TrackBusyStatus
697
+ ], ProjectManager.prototype, "getHover", null);
698
+ __decorate([
699
+ TrackBusyStatus
700
+ ], ProjectManager.prototype, "getDefinition", null);
701
+ __decorate([
702
+ TrackBusyStatus
703
+ ], ProjectManager.prototype, "getSignatureHelp", null);
704
+ __decorate([
705
+ TrackBusyStatus
706
+ ], ProjectManager.prototype, "getDocumentSymbol", null);
707
+ __decorate([
708
+ TrackBusyStatus
709
+ ], ProjectManager.prototype, "getWorkspaceSymbol", null);
710
+ __decorate([
711
+ TrackBusyStatus
712
+ ], ProjectManager.prototype, "getReferences", null);
713
+ __decorate([
714
+ TrackBusyStatus
715
+ ], ProjectManager.prototype, "getCodeActions", null);
716
+ __decorate([
717
+ TrackBusyStatus
718
+ ], ProjectManager.prototype, "createAndActivateProject", null);
719
+ __decorate([
720
+ TrackBusyStatus
721
+ ], ProjectManager.prototype, "activateProject", null);
722
+ exports.ProjectManager = ProjectManager;
723
+ /**
724
+ * An annotation used to wrap the method in a busyStatus tracking call
725
+ */
726
+ function TrackBusyStatus(target, propertyKey, descriptor) {
727
+ let originalMethod = descriptor.value;
728
+ //wrapping the original method
729
+ descriptor.value = function value(...args) {
730
+ return this.busyStatusTracker.run(() => {
731
+ return originalMethod.apply(this, args);
732
+ }, originalMethod.name);
733
+ };
734
+ }
735
+ //# sourceMappingURL=ProjectManager.js.map