UNPKG

fish-lsp

Version:

LSP implementation for fish/fish-shell

320 lines (319 loc) 13.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.workspaceManager = exports.WorkspaceManager = void 0; const vscode_languageserver_1 = require("vscode-languageserver"); const logger_1 = require("../logger"); const workspace_1 = require("./workspace"); const document_1 = require("../document"); const analyze_1 = require("../analyze"); const config_1 = require("../config"); const translation_1 = require("./translation"); class WorkspaceManager { stack = new WorkspaceStack(); allWorkspaces = new Map(); copy(workspaceManager) { this.allWorkspaces = new Map(workspaceManager.allWorkspaces); this.stack = this.stack.copy(workspaceManager.stack); return this; } setCurrent(workspace) { this.allWorkspaces.set(workspace.uri, workspace); this.stack.push(workspace); return this.stack.current; } get current() { return this.stack.current; } add(...workspaces) { workspaces.forEach((workspace) => { if (this.allWorkspaces.has(workspace.uri)) { return; } this.allWorkspaces.set(workspace.uri, workspace); }); } remove(...workspaces) { workspaces.forEach((w) => { if (this.allWorkspaces.has(w.uri)) { this.allWorkspaces.delete(w.uri); } }); this.stack.remove(...workspaces); } findContainingWorkspace(doc) { const documentUri = this.getDocumentUriFromParams(doc); return this.getWorkspaceContainingUri(documentUri); } hasContainingWorkspace(doc) { const documentUri = this.getDocumentUriFromParams(doc); return this.allWorkspaces.has(documentUri); } clear() { this.allWorkspaces.clear(); this.stack.clear(); return this; } get all() { const uniqueWorkspaces = new Set(); const result = []; this.stack.allOpened.forEach((workspace) => { result.push(workspace); uniqueWorkspaces.add(workspace.uri); }); this.allWorkspaces.forEach((workspace) => { if (!uniqueWorkspaces.has(workspace.uri)) { result.push(workspace); uniqueWorkspaces.add(workspace.uri); } }); return result; } get allUrisInAllWorkspaces() { const result = []; this.all.forEach((workspace) => { result.push(...Array.from(workspace.allUris)); }); return result; } workspacesToAnalyze() { return this.all.filter((workspace) => workspace.needsAnalysis()); } needsAnalysis() { return this.workspacesToAnalyze().length > 0; } allWorkspacesWithDocument(doc) { return this.all.filter((workspace) => workspace.contains(doc.uri)); } allAnalysisDocuments() { const uniqueUris = new Set(); const result = []; for (const workspace of this.workspacesToAnalyze()) { const pendingDocuments = workspace.pendingDocuments(); pendingDocuments.forEach((doc) => { if (!uniqueUris.has(doc.uri)) { uniqueUris.add(doc.uri); result.push(doc); } }); } return result; } get isLargeAnalysis() { return this.allAnalysisDocuments().length > 25; } findDocumentInAnyWorkspace(uri) { for (const workspace of this.all) { const doc = workspace.findDocument(d => d.uri === uri); if (doc) return doc; } return null; } getWorkspaceContainingUri(uri) { return this.all.find((workspace) => workspace.uris.has(uri) || workspace.uri === uri) || null; } getExistingWorkspaceOrCreateNew(uri) { const existingWorkspace = this.getWorkspaceContainingUri(uri); if (existingWorkspace) return existingWorkspace; const newWorkspace = workspace_1.Workspace.syncCreateFromUri(uri); if (!newWorkspace) { logger_1.logger.error(`Failed to create workspace from URI: ${uri}`); return null; } return newWorkspace; } getDocumentUriFromParams(param) { if (document_1.LspDocument.is(param)) return param.uri.toString(); if (vscode_languageserver_1.DocumentUri.is(param)) return param.toString(); if ((0, translation_1.isPath)(param)) return (0, translation_1.pathToUri)(param).toString(); return ''; } handleOpenDocument(doc) { logger_1.logger.info('workspaceManager.handleOpenDocument()', 'Opening document', doc); const documentUri = this.getDocumentUriFromParams(doc); document_1.documents.open(documentUri); const document = document_1.documents.getDocument(documentUri); const newWorkspace = this.getExistingWorkspaceOrCreateNew(documentUri); if (!newWorkspace || !document) { logger_1.logger.error('workspaceManager.handleOpenDocument()', `Failed to create or find workspace for URI: ${documentUri}`, { params: doc }); return null; } analyze_1.analyzer.analyze(document); newWorkspace.add(...Array.from(analyze_1.analyzer.collectAllSources(documentUri))); this.setCurrent(newWorkspace); if (newWorkspace.needsAnalysis()) { logger_1.logger.info(`workspaceManager.handleOpenDocument() - Workspace('${newWorkspace.name}').needsAnalysis()`); analyze_1.analyzer.analyzeWorkspace(newWorkspace); } return this.current; } handleCloseDocument(doc) { logger_1.logger.info('workspaceManager.handleCloseDocument()', 'Closing document', { params: doc }); const totalUrisBeforeRemoval = this.allUrisInAllWorkspaces.length; const documentUri = this.getDocumentUriFromParams(doc); const workspace = this.getWorkspaceContainingUri(documentUri); document_1.documents.close(documentUri); if (!workspace) { logger_1.logger.error('workspaceManager.handleCloseDocument()', `Failed to find workspace for URI: ${documentUri}`, { params: doc }); return null; } const docsInWorkspace = document_1.documents.openDocuments.filter(doc => workspace.contains(doc.uri) && this.allWorkspacesWithDocument(doc).length === 1); if (docsInWorkspace.length === 0) this.remove(workspace); logger_1.logger.info('workspaceManager.handleCloseDocument()', { priorToRemoval: totalUrisBeforeRemoval, removedUris: workspace.allUris.size, remainingUris: this.allUrisInAllWorkspaces.length, currentWorkspace: this.current?.name, removedWorkspaces: workspace.name, removedDocument: documentUri, currentDocuments: document_1.documents.openDocuments.map((doc) => doc.uri), }); return this.current || null; } handleUpdateDocument(doc) { logger_1.logger.info('workspaceManager.handleUpdateDocument()', 'Updating document:', doc); const documentUri = this.getDocumentUriFromParams(doc); const workspace = this.getExistingWorkspaceOrCreateNew(documentUri); if (!workspace) { logger_1.logger.error('workspaceManager.handleUpdateDocument()', `Failed to find workspace for URI: ${documentUri}`); return null; } this.setCurrent(workspace); const document = document_1.documents.getDocument(documentUri); if (document) { analyze_1.analyzer.analyze(document); workspace.addPending(documentUri); workspace.addPending(...Array.from(analyze_1.analyzer.collectAllSources(documentUri))); } return this.current; } handleWorkspaceChangeEvent(event, progress) { progress?.begin('[fish-lsp] indexing files', 0, `Analyzing workspaces [+${event.added.length} | -${event.removed.length}]`, true); logger_1.logger.info('workspaceManager.handleWorkspaceChangeEvent()', `Workspace change event: { added: ${event.added.length}, removed: ${event.removed.length}} `, { added: event.added.map((ws) => ws.uri), removed: event.removed.map((ws) => ws.uri), }); event.added.forEach((workspace) => { const foundWorkspace = this.getExistingWorkspaceOrCreateNew(workspace.uri); if (foundWorkspace) { this.add(foundWorkspace); } else { logger_1.logger.warning('workspaceManager.handleWorkspaceChangeEvent()', `FAILED: event.added: ${workspace.uri}`); } }); event.removed.forEach((workspace) => { const foundWorkspace = this.getExistingWorkspaceOrCreateNew(workspace.uri); if (foundWorkspace) { this.remove(foundWorkspace); } else { logger_1.logger.warning('workspaceManager.handleWorkspaceChangeEvent()', `FAILED event.removed: ${workspace.uri}`); } }); } async analyzePendingDocuments(progress = undefined, callbackfn = (s) => logger_1.logger.log(s)) { logger_1.logger.info('workspaceManager.analyzePendingDocuments()'); const items = {}; const startTime = performance.now(); const pendingDocuments = this.allAnalysisDocuments(); const maxSize = Math.min(pendingDocuments.length, config_1.config.fish_lsp_max_background_files); const currentDocuments = pendingDocuments.slice(0, maxSize); const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms)); const BATCH_SIZE = Math.max(1, Math.floor(currentDocuments.length / 20)); const UPDATE_DELAY = currentDocuments.length > 100 ? 10 : 25; let lastUpdateTime = 0; const MIN_UPDATE_INTERVAL = 15; for (let idx = 0; idx < currentDocuments.length; idx++) { const doc = currentDocuments[idx]; const workspaces = this.allWorkspacesWithDocument(doc); workspaces.forEach((workspace) => { workspace.uris.markIndexed(doc.uri); const uris = items[workspace.path] || []; uris.push(doc.uri); items[workspace.path] = uris; }); try { if (doc.getAutoloadType() === 'completions') { analyze_1.analyzer.analyzePartial(doc); } else { analyze_1.analyzer.analyze(doc); } } catch (error) { logger_1.logger.error('workspaceManager.analyzePendingDocuments()', `Error analyzing document: ${doc.uri}`, { error }); } const currentTime = performance.now(); const isLastItem = idx === currentDocuments.length - 1; const isBatchEnd = idx % BATCH_SIZE === BATCH_SIZE - 1; const timeToUpdate = currentTime - lastUpdateTime > MIN_UPDATE_INTERVAL; if (isLastItem || isBatchEnd && timeToUpdate) { const percentage = Math.ceil((idx + 1) / maxSize * 100); progress?.report(`${percentage}% Analyzing ${idx + 1}/${maxSize} ${maxSize > 1 ? 'documents' : 'document'}`); lastUpdateTime = currentTime; await delay(UPDATE_DELAY); } } const endTime = performance.now(); const duration = ((endTime - startTime) / 1000).toFixed(5); const message = `Analyzed ${currentDocuments.length} document${currentDocuments.length > 1 ? 's' : ''} in ${duration}s`; callbackfn(message); logger_1.logger.info('workspaceManager.analyzePendingDocuments()', message, { duration: `${duration}s`, totalDocuments: currentDocuments.length, maxSize, }); return { items, totalDocuments: currentDocuments.length, duration: (endTime - startTime) / 1000, }; } } exports.WorkspaceManager = WorkspaceManager; class WorkspaceStack { stack = []; copy(workspaceStack) { this.stack = [...workspaceStack.stack]; return this; } push(workspace) { if (this.has(workspace)) this.remove(workspace); this.stack.push(workspace); } pop() { return this.stack.pop(); } get current() { return this.stack[this.stack.length - 1]; } get allOpened() { return this.stack.toReversed(); } findIndex(workspace) { return this.stack.findIndex((w) => w.uri === workspace.uri); } has(workspace) { return this.stack.some((w) => w.uri === workspace.uri); } isEmpty() { return this.stack.length === 0; } clear() { this.stack = []; } get length() { return this.stack.length; } remove(...workspaces) { this.stack = this.stack.filter((w) => !workspaces.some((ws) => ws.equals(w))); } } exports.workspaceManager = new WorkspaceManager();