Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enhance LSP for multi-editor support #38

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions src/codeWalkerService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { URI } from 'vscode-uri';
import { TextDocument } from 'vscode-languageserver-textdocument';
import { ContractCollection } from './model/contractsCollection';
import { Parser } from './parser/tact';
import { DocumentStore } from './documentStore';

export class ParsedCode {
public element: any;
Expand Down Expand Up @@ -370,17 +371,19 @@ export class DocumentContract {
export class TactCodeWalker {
private rootPath: string | undefined;
private tactparser = new Parser();
private documentStore: DocumentStore;

constructor(rootPath: string | undefined) {
constructor(rootPath: string | undefined, documentStore: DocumentStore) {
this.rootPath = rootPath;
this.documentStore = documentStore;
}

public getAllContracts(document: TextDocument, position: vscode.Position): DocumentContract {
public async getAllContracts(document: TextDocument, position: vscode.Position): Promise<DocumentContract> {
let documentContract:DocumentContract = new DocumentContract();
const documentText = document.getText();
const contractPath = URI.parse(document.uri).fsPath;
const contracts = new ContractCollection();
contracts.addContractAndResolveImports(
const contracts = new ContractCollection(this.documentStore);
await contracts.addContractAndResolveImports(
contractPath,
documentText
);
Expand Down
15 changes: 9 additions & 6 deletions src/completionService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,16 @@ import { CompletionItem, CompletionItemKind } from 'vscode-languageserver';
import * as vscode from 'vscode-languageserver';
import { TextDocument } from 'vscode-languageserver-textdocument';
import {Contract2, DeclarationType, DocumentContract, Function, TactCodeWalker, Variable, Struct, Message} from './codeWalkerService';
import { DocumentStore } from './documentStore';

export class CompletionService {

public rootPath: string | undefined;
private documentStore: DocumentStore;

constructor(rootPath: string | undefined) {
constructor(rootPath: string | undefined, documentStore: DocumentStore) {
this.rootPath = rootPath;
this.documentStore = documentStore;
}

public getTypeString(literal: any) {
Expand Down Expand Up @@ -157,20 +160,20 @@ export class CompletionService {
return completionItem;
}

public getAllCompletionItems(
public async getAllCompletionItems(
document: TextDocument | undefined,
position: vscode.Position ): CompletionItem[] {
position: vscode.Position ): Promise<CompletionItem[]> {
if (document == undefined) {
return [];
}
let completionItems: CompletionItem[] = [];
let triggeredByImport = false;
let triggeredByDotStart = 0;
try {
var walker = new TactCodeWalker(this.rootPath);
var walker = new TactCodeWalker(this.rootPath, this.documentStore);
const offset = document.offsetAt(position);

var documentContractSelected = walker.getAllContracts(document, position);
var documentContractSelected = await walker.getAllContracts(document, position);

const lines = document.getText().split(/\r?\n/g);
triggeredByDotStart = this.getTriggeredByDotStart(lines, position);
Expand Down Expand Up @@ -1602,4 +1605,4 @@ function getSliceCompletionItems(): CompletionItem[] {
label: 'asString',
},
]
}
}
14 changes: 8 additions & 6 deletions src/definitionProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,15 @@ import { TextDocument } from 'vscode-languageserver-textdocument';
import { Contract } from './model/contract';
import { ContractCollection } from './model/contractsCollection';
import { Parser } from './parser/tact';
import { DocumentStore } from './documentStore';
const tactparse = new Parser();
export class TactDefinitionProvider {
private rootPath: string | undefined;
private documentStore: DocumentStore;

constructor(rootPath: string | undefined) {
constructor(rootPath: string | undefined, documentStore: DocumentStore) {
this.rootPath = rootPath;
this.documentStore = documentStore;
}

/**
Expand All @@ -24,15 +27,14 @@ export class TactDefinitionProvider {
* @returns {(Thenable<vscode.Location | vscode.Location[]>)}
* @memberof TactDefinitionProvider
*/
public provideDefinition(
public async provideDefinition(
document: TextDocument,
position: vscode.Position,
): Thenable<vscode.Location | vscode.Location[] | undefined> | undefined {
): Promise<vscode.Location | vscode.Location[] | undefined> {
const documentText = document.getText();
const contractPath = URI.parse(document.uri).fsPath;

const contracts = new ContractCollection();
contracts.addContractAndResolveImports(
const contracts = new ContractCollection(this.documentStore);
await contracts.addContractAndResolveImports(
contractPath,
documentText
);
Expand Down
109 changes: 109 additions & 0 deletions src/documentStore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import * as lsp from 'vscode-languageserver';
import { TextDocuments } from 'vscode-languageserver';
import { TextDocument } from 'vscode-languageserver-textdocument';
import { LRUMap } from './util/lruMap';

export interface TextDocumentChange2 {
document: TextDocument,
changes: {
range: lsp.Range;
rangeOffset: number;
rangeLength: number;
text: string;
}[]
}

type DocumentEntry = { exists: true, document: TextDocument } | { exists: false, document: undefined };

export class DocumentStore extends TextDocuments<TextDocument> {

private readonly _onDidChangeContent2 = new lsp.Emitter<TextDocumentChange2>();
readonly onDidChangeContent2 = this._onDidChangeContent2.event;

private readonly _decoder = new TextDecoder();
private readonly _fileDocuments: LRUMap<string, Promise<DocumentEntry>>;

constructor(private readonly _connection: lsp.Connection) {
super({
create: TextDocument.create,
update: (doc, changes, version) => {
let result: TextDocument;
let incremental = true;
let event: TextDocumentChange2 = { document: doc, changes: [] };

for (const change of changes) {
if (!lsp.TextDocumentContentChangeEvent.isIncremental(change)) {
incremental = false;
break;
}
const rangeOffset = doc.offsetAt(change.range.start);
event.changes.push({
text: change.text,
range: change.range,
rangeOffset,
rangeLength: change.rangeLength ?? doc.offsetAt(change.range.end) - rangeOffset,
});
}
result = TextDocument.update(doc, changes, version);
if (incremental) {
this._onDidChangeContent2.fire(event);
}
return result;
}
});

this._fileDocuments = new LRUMap<string, Promise<DocumentEntry>>({
size: 200,
dispose: _entries => { }
});

super.listen(_connection);
}

async retrieve(uri: string): Promise<DocumentEntry> {
let result = this.get(uri);
if (result) {
return { exists: true, document: result };
}

let promise = this._fileDocuments.get(uri);
if (!promise) {
promise = this._requestDocument(uri);
this._fileDocuments.set(uri, promise);
}
return promise;
}

private async _requestDocument(uri: string): Promise<DocumentEntry> {
const localPath = uriToPath(uri);
// Check if the file exists locally
// if (localPath && fs.existsSync(localPath)) {
// const content = fs.readFileSync(localPath, { encoding: 'utf-8' });
// const document = TextDocument.create(uri, "tact", 1, content);
// return document;
// }
const reply = await this._connection.sendRequest<{ type: string, content: String, message?: string}>('file/read', uri);
if(reply.type === 'error') {
return { exists: false, document: undefined };
}
// I am getting buffer from server, so I need to convert it to string
// to create TextDocument
const content = Buffer.from(reply.content).toString('utf-8');
const document = TextDocument.create(uri, "tact", 1, content);
return { exists: true, document: document };
}

// remove "file document", e.g one that has been retrieved from "disk"
// and not one that is sync'd by LSP
removeFile(uri: string) {
return this._fileDocuments.delete(uri);
}
}


function uriToPath(uri: string): string | null {
if (uri.startsWith('file://')) {
return uri.slice(7); // Remove 'file://' from URI to get the path
}
return null;
}
40 changes: 18 additions & 22 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,32 +6,12 @@ import {
} from 'vscode-languageclient/node';

// tslint:disable-next-line:no-duplicate-imports
import {
workspace, ExtensionContext, DiagnosticCollection,
languages, TextDocument,
FormattingOptions, CancellationToken,
ProviderResult, TextEdit,
DocumentSelector
} from 'vscode';

import { formatDocument } from './formatter';
import { workspace, ExtensionContext, Uri, window, FileSystemError } from 'vscode';

let diagnosticCollection: DiagnosticCollection;
let clientDisposable: LanguageClient;

export async function activate(context: ExtensionContext) {
const ws = workspace.workspaceFolders;
diagnosticCollection = languages.createDiagnosticCollection('tact');

context.subscriptions.push(diagnosticCollection);

context.subscriptions.push(
languages.registerDocumentFormattingEditProvider('tact', {
provideDocumentFormattingEdits(document: TextDocument, options: FormattingOptions, token: CancellationToken): ProviderResult<TextEdit[]> {
return Promise.resolve(formatDocument(document, context));
},
})
);

const serverModule = path.join(__dirname, './server.js');

Expand Down Expand Up @@ -73,6 +53,22 @@ export async function activate(context: ExtensionContext) {
});

clientDisposable.start();

clientDisposable.onReady().then(() => {
clientDisposable.onRequest('file/read', async raw => {
const uri = Uri.parse(raw);
try {
const content = await workspace.fs.readFile(uri);
return { type: 'success', content: content.toString() };
} catch (error) {
if (error instanceof FileSystemError) {
return { type: 'error', message: 'File not found'};
}
console.warn(error);
return { type: 'error', message: 'Unknown error'};
}
});
})
}
}

Expand All @@ -81,4 +77,4 @@ export function deactivate(): Thenable<void> | undefined {
return undefined;
}
return clientDisposable.stop();
}
}
62 changes: 33 additions & 29 deletions src/formatter.ts
Original file line number Diff line number Diff line change
@@ -1,40 +1,44 @@
import * as prettier from 'prettier';
import { workspace, ExtensionContext, Range, TextDocument, TextEdit } from 'vscode';
import * as path from 'path';
import { TextEdit, Range } from 'vscode-languageserver-types';
import { TextDocument } from 'vscode-languageserver-textdocument';

export function formatDocument(document: TextDocument, context: ExtensionContext): TextEdit[] | undefined {
const rootPath = workspace.getWorkspaceFolder(document.uri)?.name;

const ignoreOptions = { ignorePath: path.join(rootPath ? rootPath: "", '.prettierignore') };

const fileInfo = prettier.getFileInfo.sync(document.uri.fsPath, ignoreOptions);

if (!fileInfo.ignored) {
const source = document.getText();
// @TODO create npm module later
const pluginPath = path.join(context.extensionPath, 'out', 'prettier-plugin-tact', 'prettier-plugin-tact.js'); // 'node_modules',
const options = {
export function formatDocument(document: TextDocument, rootPath: string): TextEdit[] | undefined {

if (rootPath) {
const ignoreOptions = { ignorePath: path.join(rootPath, '.prettierignore') };
const fileInfo = prettier.getFileInfo.sync(document.uri, ignoreOptions);
if (fileInfo.ignored) return;
}

const lspPath = __dirname;

const source = document.getText();
// @TODO create npm module later
const pluginPath = path.join(lspPath, 'prettier-plugin-tact', 'prettier-plugin-tact.js'); // 'node_modules',
const options = {
'parser': 'tact-parser',
'pluginSearchDirs': [context.extensionPath],
'pluginSearchDirs': [lspPath],
'plugins': [pluginPath],
};
//
const config = prettier.resolveConfig.sync(document.uri.fsPath);
if (config !== null) {
};

const config = prettier.resolveConfig.sync(document.uri);

if (config !== null) {
prettier.clearConfigCache();
}
Object.assign(options, config);

const firstLine = document.lineAt(0);
const lastLine = document.lineAt(document.lineCount - 1);
const fullTextRange = new Range(firstLine.range.start, lastLine.range.end);
let formatted = "";
try {
}
Object.assign(options, config);

const startIndex = document.positionAt(0);
const endIndex = document.positionAt(document.getText().length);

const fullTextRange = Range.create(startIndex, endIndex);
let formatted = "";
try {
formatted = prettier.format(source, options);
} catch(e) {
} catch (e) {
formatted = source;
console.log(e);
}
return [TextEdit.replace(fullTextRange, formatted)];
}
return [TextEdit.replace(fullTextRange, formatted)];
}
Loading