fish-lsp
Version:
LSP implementation for fish/fish-shell
320 lines (319 loc) • 13.2 kB
JavaScript
"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();