UNPKG

fish-lsp

Version:

LSP implementation for fish/fish-shell

294 lines (293 loc) 9.99 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.LspDocuments = exports.LspDocument = void 0; const fs_1 = require("fs"); const vscode_languageserver_textdocument_1 = require("vscode-languageserver-textdocument"); const vscode_languageserver_1 = require("vscode-languageserver"); const vscode_uri_1 = require("vscode-uri"); const os_1 = require("os"); const translation_1 = require("./utils/translation"); class LspDocument { document; constructor(doc) { const { uri, languageId, version, text } = doc; this.document = vscode_languageserver_textdocument_1.TextDocument.create(uri, languageId, version, text); } get uri() { return this.document.uri; } get languageId() { return this.document.languageId; } get version() { return this.document.version; } getText(range) { return this.document.getText(range); } positionAt(offset) { return this.document.positionAt(offset); } offsetAt(position) { return this.document.offsetAt(position); } get lineCount() { return this.document.lineCount; } /** * @see getLineBeforeCursor() */ getLine(line) { const lineRange = this.getLineRange(line); return this.getText(lineRange); } getLineBeforeCursor(position) { const lineStart = vscode_languageserver_1.Position.create(position.line, 0); const lineEnd = vscode_languageserver_1.Position.create(position.line, position.character); const lineRange = vscode_languageserver_1.Range.create(lineStart, lineEnd); return this.getText(lineRange); } getLineRange(line) { const lineStart = this.getLineStart(line); const lineEnd = this.getLineEnd(line); return vscode_languageserver_1.Range.create(lineStart, lineEnd); } getLineEnd(line) { const nextLineOffset = this.getLineOffset(line + 1); return this.positionAt(nextLineOffset - 1); } getLineOffset(line) { const lineStart = this.getLineStart(line); return this.offsetAt(lineStart); } getLineStart(line) { return vscode_languageserver_1.Position.create(line, 0); } getIndentAtLine(line) { const lineText = this.getLine(line); const indent = lineText.match(/^\s+/); return indent ? indent[0] : ''; } applyEdits(version, ...changes) { for (const change of changes) { const content = this.getText(); let newContent = change.text; if (vscode_languageserver_1.TextDocumentContentChangeEvent.isIncremental(change)) { const start = this.offsetAt(change.range.start); const end = this.offsetAt(change.range.end); newContent = content.substring(0, start) + change.text + content.substring(end); } this.document = vscode_languageserver_textdocument_1.TextDocument.create(this.uri, this.languageId, version, newContent); } } rename(newUri) { this.document = vscode_languageserver_textdocument_1.TextDocument.create(newUri, this.languageId, this.version, this.getText()); } getFilePath() { return (0, translation_1.uriToPath)(this.uri); } getFilename() { return this.uri.split('/').pop(); } getRelativeFilenameToWorkspace() { const home = (0, os_1.homedir)(); const path = this.uri.replace(home, '~'); const dirs = path.split('/'); const workspaceRootIndex = dirs.find(dir => dir === 'fish') ? dirs.indexOf('fish') : dirs.find(dir => ['conf.d', 'functions', 'completions', 'config.fish'].includes(dir)) ? dirs.findLastIndex(dir => ['conf.d', 'functions', 'completions', 'config.fish'].includes(dir)) : dirs.length - 1; return dirs.slice(workspaceRootIndex).join('/'); } /** * checks if the functions are defined in a functions directory */ isFunction() { const pathArray = this.uri.split('/'); const fileName = pathArray.pop(); const parentDir = pathArray.pop(); /** paths that autoload all top level functions to the shell env */ if (parentDir === 'conf.d' || fileName === 'config.fish') { return true; } /** path that autoload matching filename functions to the shell env */ return parentDir === 'functions'; } shouldAnalyzeInBackground() { const pathArray = this.uri.split('/'); const fileName = pathArray.pop(); const parentDir = pathArray.pop(); return parentDir && ['functions', 'conf.d'].includes(parentDir?.toString()) || fileName === 'config.fish'; } /** * checks if the document is in a location where the functions * that it defines are autoloaded by fish. * * Use isAutoloadedUri() if you want to check for completions * files as well. This function does not check for completion * files. */ isAutoloaded() { const path = (0, translation_1.uriToPath)(this.uri); if (path?.includes('fish/functions')) { return true; } else if (path?.includes('fish/conf.d')) { return true; } else if (path?.includes('fish/config.fish')) { return true; } return false; } /** * checks if the document is in a location: * - `fish/{conf.d,functions,completions}/file.fish` * - `fish/config.fish` * * Key difference from isAutoLoaded is that this function checks for * completions files as well. isAutoloaded() does not check for * completion files. */ isAutoloadedUri() { const path = (0, translation_1.uriToPath)(this.uri); if (path?.includes('fish/functions')) { return true; } else if (path?.includes('fish/conf.d')) { return true; } else if (path?.includes('fish/config.fish')) { return true; } else if (path?.includes('fish/completions')) { return true; } return false; } /** * helper that gets the document URI if it is fish/functions directory */ getAutoloadType() { const path = (0, translation_1.uriToPath)(this.uri); if (path?.includes('fish/functions')) { return 'functions'; } else if (path?.includes('fish/conf.d')) { return 'conf.d'; } else if (path?.includes('fish/config.fish')) { return 'config'; } else if (path?.includes('fish/completions')) { return 'completions'; } return ''; } /** * helper that gets the document URI if it is fish/functions directory * @returns {string} - what the function name should be, or '' if it is not autoloaded */ getAutoLoadName() { if (!this.isAutoloaded()) { return ''; } const parts = (0, translation_1.uriToPath)(this.uri)?.split('/') || []; const name = parts[parts.length - 1]; return name.replace('.fish', ''); } getLines() { const lines = this.getText().split('\n'); return lines.length; } } exports.LspDocument = LspDocument; class LspDocuments { _files = []; documents = new Map(); loadingQueue = new Set(); loadedFiles = new Map(); // uri -> timestamp /** * Sorted by last access. */ get files() { return this._files; } get(file) { if (!file) { return undefined; } const document = this.documents.get(file); if (!document) { return undefined; } if (this.files[0] !== file) { this._files.splice(this._files.indexOf(file), 1); this._files.unshift(file); } return document; } // Enhanced get method that supports async loading async getAsync(uri) { if (!uri) return undefined; return this.getDocument(uri); } async getDocument(uri) { if (!this.loadingQueue.has(uri) && !this.loadedFiles.has(uri)) { this.loadingQueue.add(uri); try { const content = await fs_1.promises.readFile((0, translation_1.uriToPath)(uri), 'utf8'); const doc = new LspDocument({ uri, languageId: 'fish', version: 1, text: content, }); this.documents.set(uri, doc); this.loadedFiles.set(uri, Date.now()); } finally { this.loadingQueue.delete(uri); } } return this.documents.get(uri); } open(file, doc) { if (this.documents.has(file)) { return false; } this.documents.set(file, new LspDocument(doc)); this._files.unshift(file); return true; } close(file) { const document = this.documents.get(file); if (!document) { return undefined; } this.documents.delete(file); this._files.splice(this._files.indexOf(file), 1); return document; } rename(oldFile, newFile) { const document = this.documents.get(oldFile); if (!document) { return false; } document.rename(newFile); this.documents.delete(oldFile); this.documents.set(newFile, document); this._files[this._files.indexOf(oldFile)] = newFile; return true; } toResource(filepath) { const document = this.documents.get(filepath); if (document) { return vscode_uri_1.URI.parse(document.uri); } return vscode_uri_1.URI.file(filepath); } } exports.LspDocuments = LspDocuments;