fish-lsp
Version:
LSP implementation for fish/fish-shell
484 lines (483 loc) • 17.3 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.documents = 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 path = __importStar(require("path"));
const vscode_uri_1 = require("vscode-uri");
const os_1 = require("os");
const translation_1 = require("./utils/translation");
const workspace_manager_1 = require("./utils/workspace-manager");
const file_operations_1 = require("./utils/file-operations");
const logger_1 = require("./logger");
const Locations = __importStar(require("./utils/locations"));
const symbol_1 = require("./parsing/symbol");
class LspDocument {
document;
constructor(doc) {
const { uri, languageId, version, text } = doc;
this.document = vscode_languageserver_textdocument_1.TextDocument.create(uri, languageId, version, text);
}
static createTextDocumentItem(uri, text) {
return new LspDocument({
uri,
languageId: 'fish',
version: 1,
text,
});
}
static fromTextDocument(doc) {
const item = vscode_languageserver_1.TextDocumentItem.create(doc.uri, doc.languageId, doc.version, doc.getText());
return new LspDocument(item);
}
static createFromUri(uri) {
const content = file_operations_1.SyncFileHelper.read((0, translation_1.uriToPath)(uri));
return LspDocument.createTextDocumentItem(uri, content);
}
static createFromPath(path) {
const content = file_operations_1.SyncFileHelper.read(path);
return LspDocument.createTextDocumentItem((0, translation_1.pathToUri)(path), content);
}
static testUri(uri) {
const removeString = 'test-data/workspaces';
if (uri.includes(removeString)) {
return 'file:///…/' + uri.slice(uri.indexOf(removeString) + removeString.length + 1);
}
return uri;
}
static testUtil(uri) {
const shortUri = LspDocument.testUri(uri);
const fullPath = (0, translation_1.uriToPath)(uri);
const parentDir = path.dirname(fullPath);
const relativePath = shortUri.slice(shortUri.indexOf(parentDir) + parentDir.length + 1);
return {
uri,
shortUri,
fullPath,
relativePath,
parentDir,
};
}
static create(param) {
if (typeof param === 'string' && (0, translation_1.isPath)(param))
return LspDocument.createFromPath(param);
if (typeof param === 'string' && (0, translation_1.isUri)(param))
return LspDocument.createFromUri(param);
if (LspDocument.is(param))
return LspDocument.fromTextDocument(param.document);
if ((0, translation_1.isTextDocumentItem)(param))
return LspDocument.createTextDocumentItem(param.uri, param.text);
if ((0, translation_1.isTextDocument)(param))
return LspDocument.fromTextDocument(param);
logger_1.logger.error('Invalid parameter type `LspDocument.create()`: ', param);
return undefined;
}
static async createFromUriAsync(uri) {
const content = await fs_1.promises.readFile((0, translation_1.uriToPath)(uri), 'utf8');
return LspDocument.createTextDocumentItem(uri, content);
}
asTextDocumentItem() {
return {
uri: this.document.uri,
languageId: this.document.languageId,
version: this.document.version,
text: this.document.getText(),
};
}
asTextDocumentIdentifier() {
return {
uri: this.document.uri,
};
}
get uri() {
return this.document.uri;
}
get languageId() {
return this.document.languageId;
}
get version() {
return this.document.version;
}
get path() {
return (0, translation_1.uriToPath)(this.document.uri);
}
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;
}
create(uri, languageId, version, text) {
return new LspDocument({
uri,
languageId: languageId || 'fish',
version: version || 1,
text,
});
}
getLine(line) {
if (Locations.Position.is(line)) {
line = line.line;
}
else if (Locations.Range.is(line)) {
line = line.start.line;
}
else if (symbol_1.FishSymbol.is(line)) {
line = line.range.start.line;
}
const lines = this.document.getText().split('\n');
return lines[line] || '';
}
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] : '';
}
update(changes) {
this.document = vscode_languageserver_textdocument_1.TextDocument.update(this.document, changes, this.version + 1);
return LspDocument.fromTextDocument(this.document);
}
asVersionedIdentifier() {
return vscode_languageserver_1.VersionedTextDocumentIdentifier.create(this.uri, this.version);
}
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.findIndex(dir => ['conf.d', 'functions', 'completions', 'config.fish'].includes(dir))
: dirs.length - 1;
return dirs.slice(workspaceRootIndex).join('/');
}
isFunction() {
const pathArray = this.uri.split('/');
const fileName = pathArray.pop();
const parentDir = pathArray.pop();
if (parentDir === 'conf.d' || fileName === 'config.fish') {
return true;
}
return parentDir === 'functions';
}
isAutoloadedFunction() {
return this.getAutoloadType() === 'functions';
}
isAutoloadedCompletion() {
return this.getAutoloadType() === 'completions';
}
isAutoloadedConfd() {
return this.getAutoloadType() === 'conf.d';
}
shouldAnalyzeInBackground() {
const pathArray = this.uri.split('/');
const fileName = pathArray.pop();
const parentDir = pathArray.pop();
return parentDir && ['functions', 'conf.d', 'completions'].includes(parentDir?.toString()) || fileName === 'config.fish';
}
getWorkspace() {
return workspace_manager_1.workspaceManager.findContainingWorkspace(this.uri) || undefined;
}
getFolderType() {
const docPath = (0, translation_1.uriToPath)(this.uri);
if (!docPath)
return null;
const dirName = path.basename(path.dirname(docPath));
const fileName = path.basename(docPath);
if (dirName === 'functions')
return 'functions';
if (dirName === 'conf.d')
return 'conf.d';
if (dirName === 'completions')
return 'completions';
if (fileName === 'config.fish')
return 'config';
return '';
}
isAutoloaded() {
const folderType = this.getFolderType();
if (!folderType)
return false;
return ['functions', 'conf.d', 'config'].includes(folderType);
}
isAutoloadedUri() {
const folderType = this.getFolderType();
if (!folderType)
return false;
return ['functions', 'conf.d', 'config', 'completions'].includes(folderType);
}
isAutoloadedWithPotentialCompletions() {
const folderType = this.getFolderType();
if (!folderType)
return false;
return ['conf.d', 'config', 'completions'].includes(folderType);
}
getAutoloadType() {
return this.getFolderType() || '';
}
getAutoLoadName() {
if (!this.isAutoloadedUri()) {
return '';
}
const parts = (0, translation_1.uriToPath)(this.uri)?.split('/') || [];
const name = parts[parts.length - 1];
return name.replace('.fish', '');
}
getFileName() {
const items = (0, translation_1.uriToPath)(this.uri).split('/') || [];
const name = items.length > 0 ? items.pop() : (0, translation_1.uriToPath)(this.uri);
return name;
}
getLines() {
const lines = this.getText().split('\n');
return lines.length;
}
static is(value) {
return (typeof value === 'object' &&
value !== null &&
typeof value.asTextDocumentItem === 'function' &&
typeof value.asTextDocumentIdentifier === 'function' &&
typeof value.getAutoloadType === 'function' &&
typeof value.isAutoloaded === 'function' &&
typeof value.path === 'string' &&
typeof value.getFileName === 'function' &&
typeof value.getRelativeFilenameToWorkspace === 'function' &&
typeof value.getLine === 'function' &&
typeof value.getLines === 'function' &&
typeof value.uri === 'string' &&
typeof value.getText === 'function');
}
}
exports.LspDocument = LspDocument;
class LspDocuments {
_files = [];
documents = new Map();
static create() {
return new LspDocuments();
}
static from(documents) {
const newDocuments = new LspDocuments();
newDocuments.documents.clear();
newDocuments._files.length = 0;
documents.documents.forEach((doc, file) => {
newDocuments.documents.set(file, doc);
newDocuments._files.push(file);
});
return newDocuments;
}
copy(documents) {
this.documents.clear();
this._files.push(...documents._files);
documents.documents.forEach((doc, file) => {
this.documents.set(file, doc);
});
return this;
}
get openDocuments() {
const result = [];
for (const file of this._files.toReversed()) {
const document = this.documents.get(file);
if (document) {
result.push(document);
}
}
return result;
}
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;
}
openPath(path, doc) {
const lspDocument = new LspDocument(doc);
this.documents.set(path, lspDocument);
this._files.unshift(path);
return lspDocument;
}
getPathFromParam(param) {
if ((0, translation_1.isUri)(param)) {
return (0, translation_1.uriToPath)(param);
}
if ((0, translation_1.isPath)(param)) {
return param;
}
if ((0, translation_1.isTextDocument)(param)) {
return (0, translation_1.uriToPath)(param.uri);
}
if ((0, translation_1.isTextDocumentItem)(param)) {
return (0, translation_1.uriToPath)(param.uri);
}
if (LspDocument.is(param)) {
return param.path;
}
throw new Error('Invalid parameter type');
}
open(param) {
const path = this.getPathFromParam(param);
if (this.documents.has(path)) {
return false;
}
const newDoc = LspDocument.create(param);
this.documents.set(path, newDoc);
this._files.unshift(path);
return true;
}
isOpen(path) {
if (vscode_uri_1.URI.isUri(path)) {
path = (0, translation_1.uriToPath)(path);
}
return this.documents.has(path);
}
get uris() {
return Array.from(this._files).map(file => (0, translation_1.pathToUri)(file));
}
getDocument(uri) {
const path = (0, translation_1.uriToPath)(uri);
return this.documents.get(path);
}
openTextDocument(document) {
const path = (0, translation_1.uriToPath)(document.uri);
if (this.documents.has(path)) {
return this.documents.get(path);
}
const lspDocument = LspDocument.fromTextDocument(document);
this.documents.set(path, lspDocument);
this._files.unshift(path);
return lspDocument;
}
updateTextDocument(textDocument) {
const path = (0, translation_1.uriToPath)(textDocument.uri);
this.documents.set(path, LspDocument.fromTextDocument(textDocument));
return this.documents.get(path);
}
applyChanges(uri, changes) {
const path = (0, translation_1.uriToPath)(uri);
let document = this.documents.get(path);
if (document) {
document = document.update(changes);
this.documents.set(path, document);
}
}
set(document) {
const path = (0, translation_1.uriToPath)(document.uri);
this.documents.set(path, document);
}
all() {
return Array.from(this.documents.values());
}
closeTextDocument(document) {
const path = (0, translation_1.uriToPath)(document.uri);
return this.close(path);
}
close(param) {
const path = this.getPathFromParam(param);
const document = this.documents.get(path);
if (!document) {
return undefined;
}
this.documents.delete(path);
this._files.splice(this._files.indexOf(path), 1);
return document;
}
closeAll() {
this.documents.clear();
this._files.length = 0;
}
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);
}
clear() {
this.documents.clear();
this._files.length = 0;
}
}
exports.LspDocuments = LspDocuments;
exports.documents = LspDocuments.create();