13 Commits
0.2.0 ... main

Author SHA1 Message Date
5945c04cce Merge pull request #2 from chrisvrose/exec-terminal
Execution Tasks
2025-07-20 04:35:07 +01:00
56e27a2d14 [readme] 2025-07-20 04:31:53 +01:00
3f8f515262 [fix] aborting 2025-07-20 04:12:13 +01:00
3656d71e6d [add] branflakes execution 2025-07-20 03:36:43 +01:00
1ce0b2e4e2 [format] 2025-07-20 00:56:08 +01:00
9ab72c9ccb [add] task configs 2025-07-20 00:02:45 +01:00
6f5357ea5c [fix] updates 2025-07-19 18:50:14 +01:00
b2014942b6 [fix] 2025-07-19 15:15:43 +01:00
e549845d70 [refactor] 2025-07-19 14:39:44 +01:00
7e5b68116a [fix] tabs 2025-07-19 11:34:01 +01:00
10850c9d98 [add] indirection 2025-07-19 11:17:53 +01:00
e8ce9b73ae [readme] 2024-01-02 22:39:31 +05:30
fc7e3b431f [reword] 2024-01-02 22:32:24 +05:30
24 changed files with 4011 additions and 5312 deletions

1
.gitignore vendored
View File

@@ -1,3 +1,4 @@
client/.antlr
test.bf test.bf
out out
dist dist

View File

@@ -1,9 +1,8 @@
{ {
"editor.insertSpaces": false,
"tslint.enable": true, "tslint.enable": true,
"typescript.tsc.autoDetect": "off", "typescript.tsc.autoDetect": "off",
"typescript.preferences.quoteStyle": "single", "typescript.preferences.quoteStyle": "single",
"editor.codeActionsOnSave": { "editor.codeActionsOnSave": {
"source.fixAll.eslint": true "source.fixAll.eslint": "explicit"
} }
} }

View File

@@ -13,14 +13,30 @@ A simple language server based VSCode Extension for the (Branflakes?) (BrainFuck
### Execution ### Execution
Use the command to execute the code. Use the BF execute task to execute the code.
Issue is, because BF is a **turing complete** language, there is no way to know if the program will terminate or not. Hence for now, the command may lead to infinite execution. Either run the "current file" task, or create a customized task with the required file.
If the program requires input, it will be requested as a prompt. I/O is done on the created terminal window.
TODO: Implement a timeout.
There is also an older command to run the code, where output is shown as a status message. Here, if the program requires input, it will be requested as a prompt.
### Changelog ### Changelog
#### 0.3.0
![command](./assets/command.gif)
- Added a task for execution
- Press Control C to halt it while its waiting for input
- Close task to abort execution
- Detail: The program will halt between loop iterations.
- Migrated the run command to `bf.execute.old`
#### 0.2.1
- Change category
- Small bugfix for brackets validation
#### 0.2.0 #### 0.2.0
- Cycle input pointer on overflow/underflow - Cycle input pointer on overflow/underflow

BIN
assets/command.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 540 KiB

View File

@@ -0,0 +1,20 @@
import { EventEmitter } from 'vscode';
import InputStrategy from './input/InputStrategy';
import {BranFlakesExecutorVisitor} from './exec/BranFlakesExecutorVisitor';
export class BranFlakesStreamingExecutor {
constructor(private fileData: string, private fileName: string = 'fileName:dummy', private emitter: EventEmitter<string>,
private inputStrategy: InputStrategy
) { }
async run() {
const finalOutput = await BranFlakesExecutorVisitor.run(
this.fileData,
this.fileName,
this.inputStrategy,
async (str) => { this.emitter.fire(str); }
);
}
}

View File

@@ -1,140 +0,0 @@
import { AbstractParseTreeVisitor } from 'antlr4ts/tree/AbstractParseTreeVisitor';
import { LoopStmtContext } from './generated/bfParser';
import { bfVisitor } from './generated/bfVisitor';
import { DiagnosticSeverity } from 'vscode-languageclient';
import { getTree } from './BranFlakesParseRunner';
import { RuleNode } from 'antlr4ts/tree/RuleNode';
import InputStrategy from './input/InputStrategy';
export default class BranFlakesExecutorVisitor
extends AbstractParseTreeVisitor<Promise<void>>
implements bfVisitor<Promise<void>>
{
/**
*
* @param input Input string
* @param inputPtr Input pointer to start from
*/
constructor(
private inputStrategy: InputStrategy,
private logger: (val: string) => Thenable<string>,
private inputPtr: number = 0
) {
super();
}
// /**
// * The memory cells (Can work with negative cells this way)
// */
// private cells: Map<number, number> = new Map();
private byteArraySize: number = 30000;
private byteArray: Int8Array = new Int8Array(this.byteArraySize);
/**
* Pointer
*/
private ptr: number = 0;
/** Output string */
private outputStr: string = '';
defaultResult() {
return Promise.resolve();
}
/**
* Run a file
* @param text
* @param fn
* @param inputStrategy
* @returns
*/
static async run(
text: string,
fn: string,
inputStrategy: InputStrategy,
logger: (str: string) => Thenable<string>
) {
//get tree and issues
const { tree, issues } = getTree(text, fn);
//get only errors
const x = issues.filter(e => e.type === DiagnosticSeverity.Error);
//if any error, drop
if (x.length > 0) {
throw Error('Errors exist');
}
// make visitor
const vis = new BranFlakesExecutorVisitor(inputStrategy, logger);
//visit the tree
await vis.visit(tree);
//get output
return vis.outputStr;
}
getCell(pointerIndex: number) {
return this.byteArray[pointerIndex];
}
setCell(pointerIndex: number, value: number): void {
this.byteArray[pointerIndex] = value;
}
async visitLoopStmt(ctx: LoopStmtContext) {
while ((this.getCell(this.ptr) ?? 0) !== 0) {
await this.visitChildren(ctx);
}
}
async visitPtrLeft() {
this.ptr = (this.ptr + this.byteArraySize - 1) % this.byteArraySize;
}
async visitPtrRight() {
this.ptr = (this.ptr + this.byteArraySize + 1) % this.byteArraySize;
}
async visitPtrIncr() {
const val = this.getCell(this.ptr);
this.setCell(this.ptr, (val + 1) % 256);
}
async visitPtrDecr() {
const val = this.getCell(this.ptr);
this.setCell(this.ptr, (val + 255) % 256);
}
async visitOutputStmt() {
const val = this.getCell(this.ptr) ?? 0;
const str = String.fromCharCode(val);
this.outputStr += str;
}
async visitInputStmt() {
//get char
const char = (await this.inputStrategy.getInput()) ?? 0;
//increment the input pointer after this
this.inputPtr++;
this.setCell(this.ptr, char);
}
// override for maintaining async
async visitChildren(node: RuleNode): Promise<void> {
let result = this.defaultResult();
await result;
let n = node.childCount;
for (let i = 0; i < n; i++) {
if (!this.shouldVisitNextChild(node, result)) {
break;
}
let c = node.getChild(i);
let childResult = c.accept(this);
result = this.aggregateResult(result, childResult);
await result;
}
return Promise.resolve();
}
// override for maintaining async
protected async aggregateResult(
aggregate: Promise<void>,
nextResult: Promise<void>
): Promise<void> {
await aggregate;
return nextResult;
}
}

View File

@@ -0,0 +1,4 @@
export interface BranFlakesCommand {
getCommandName(): string;
getCommandHandler(): (...args: any) => Promise<any>;
}

View File

@@ -1,5 +0,0 @@
export interface Command{
getCommandName():string;
getCommandHandler():(...args:any)=>Promise<any>;
}

View File

@@ -0,0 +1,29 @@
import { window } from 'vscode';
import type { BranFlakesCommand } from './BranFlakesCommand';
import { VSCodePromptInputStrategy } from '../input/VSCodePromptInputStrategy';
export class CompileBranFlakesCommand implements BranFlakesCommand {
getCommandName() {
return 'bf.execute.old';
}
getCommandHandler() {
return async () => {
const text = window.activeTextEditor.document.getText();
const fn = window.activeTextEditor.document.fileName;
const inputStrategy = new VSCodePromptInputStrategy(
window.showInputBox
);
const { BranFlakesExecutorVisitor } = await import(
'../exec/BranFlakesExecutorVisitor'
);
const output = await BranFlakesExecutorVisitor.run(
text,
fn,
inputStrategy,
window.showInformationMessage
);
await window.showInformationMessage(`Output: ${output}`);
};
}
}

View File

@@ -1,26 +0,0 @@
import { window } from 'vscode';
import { Command as BranFlakesCommand } from './Command';
import { VSCodePromptInputStrategy } from '../input/VSCodePromptInputStrategy';
import BranFlakesExecutorVisitor from '../BranFlakesExecutorVisitor';
export class CompileBranFlakesCommand implements BranFlakesCommand {
getCommandName() {
return 'bf.execute';
}
getCommandHandler() {
return async () => {
const text = window.activeTextEditor.document.getText();
const fn = window.activeTextEditor.document.fileName;
const inputStrategy = new VSCodePromptInputStrategy(
window.showInputBox
);
const output = await BranFlakesExecutorVisitor.run(
text,
fn,
inputStrategy,
window.showInformationMessage
);
await window.showInformationMessage(`Output: ${output}`);
};
}
}

View File

@@ -0,0 +1,11 @@
export class AbortClassRequestor{
private abortRequest=false;
requestAbort(){
this.abortRequest=true;
}
isAborted(){
return this.abortRequest;
}
}

View File

@@ -0,0 +1,147 @@
import { AbstractParseTreeVisitor } from 'antlr4ts/tree/AbstractParseTreeVisitor';
import { LoopStmtContext } from '../generated/bfParser';
import { bfVisitor } from '../generated/bfVisitor';
import { DiagnosticSeverity } from 'vscode-languageclient';
import { getTree } from '../BranFlakesParseRunner';
import { RuleNode } from 'antlr4ts/tree/RuleNode';
import type InputStrategy from '../input/InputStrategy';
import { AbortClassRequestor } from './AbortRequestor';
export class BranFlakesExecutorVisitor
extends AbstractParseTreeVisitor<Promise<void>>
implements bfVisitor<Promise<void>> {
/**
*
* @param input Input string
* @param inputPtr Input pointer to start from
*/
constructor(
private inputStrategy: InputStrategy,
private logger: (val: string) => Thenable<void>,
private abortRequestor?: AbortClassRequestor,
private inputPtr: number = 0
) {
super();
}
// /**
// * The memory cells (Can work with negative cells this way)
// */
// private cells: Map<number, number> = new Map();
private byteArraySize: number = 30000;
private byteArray: Int8Array = new Int8Array(this.byteArraySize);
/**
* Pointer
*/
private ptr: number = 0;
/** Output string */
private outputStr: string = '';
defaultResult() {
return Promise.resolve();
}
/**
* Run a file
* @param text
* @param fn
* @param inputStrategy
* @returns
*/
static async run(
text: string,
fn: string,
inputStrategy: InputStrategy,
logger: (str: string) => Thenable<void>,
aborter?: AbortClassRequestor
) {
//get tree and issues
const { tree, issues } = getTree(text, fn);
//get only errors
const x = issues.filter(e => e.type === DiagnosticSeverity.Error);
//if any error, drop
if (x.length > 0) {
throw Error('Errors exist');
}
// make visitor
const vis = new BranFlakesExecutorVisitor(inputStrategy, logger, aborter);
//visit the tree
await vis.visit(tree);
//get output
return vis.outputStr;
}
getCell(pointerIndex: number) {
return this.byteArray[pointerIndex];
}
setCell(pointerIndex: number, value: number): void {
this.byteArray[pointerIndex] = value;
}
async visitLoopStmt(ctx: LoopStmtContext) {
while ((this.getCell(this.ptr) ?? 0) !== 0) {
await this.visitChildren(ctx);
}
}
async visitPtrLeft() {
this.ptr = (this.ptr + this.byteArraySize - 1) % this.byteArraySize;
}
async visitPtrRight() {
this.ptr = (this.ptr + this.byteArraySize + 1) % this.byteArraySize;
}
async visitPtrIncr() {
const val = this.getCell(this.ptr);
this.setCell(this.ptr, (val + 1) % 256);
}
async visitPtrDecr() {
const val = this.getCell(this.ptr);
this.setCell(this.ptr, (val + 255) % 256);
}
async visitOutputStmt() {
const val = this.getCell(this.ptr) ?? 0;
const str = String.fromCharCode(val);
this.outputStr += str;
await this.logger(str);
}
async visitInputStmt() {
//get char
const char = (await this.inputStrategy.getInput()) ?? 0;
if (char === 3) { throw Error('Halt input wait'); }
//increment the input pointer after this
this.inputPtr++;
this.setCell(this.ptr, char);
}
// override for maintaining async
async visitChildren(node: RuleNode): Promise<void> {
let result = this.defaultResult();
await result;
let n = node.childCount;
for (let i = 0; i < n; i++) {
if (!this.shouldVisitNextChild(node, result)) {
break;
}
let c = node.getChild(i);
let childResult = c.accept(this);
result = this.aggregateResult(result, childResult);
await result;
}
// break for any close requests
return new Promise((res, rej) => {
setTimeout(() => { if (this.abortRequestor?.isAborted()??false) {rej('aborted');} else {res(undefined);} }, 0);
});
}
// override for maintaining async
protected async aggregateResult(
aggregate: Promise<void>,
nextResult: Promise<void>
): Promise<void> {
await aggregate;
return nextResult;
}
}

View File

@@ -4,17 +4,21 @@
* ------------------------------------------------------------------------------------------ */ * ------------------------------------------------------------------------------------------ */
import * as path from 'path'; import * as path from 'path';
import { ExtensionContext, commands, window } from 'vscode'; import { commands, tasks, workspace,window } from 'vscode';
import type { Disposable, ExtensionContext } from 'vscode';
import { import {
LanguageClient, LanguageClient,
LanguageClientOptions, LanguageClientOptions,
ServerOptions, ServerOptions,
TransportKind, TransportKind,
} from 'vscode-languageclient'; } from 'vscode-languageclient';
import { CompileBranFlakesCommand } from './command/CompileCommand'; import { CompileBranFlakesCommand } from './command/CompileBranFlakesCommand';
import { CustomExecutionTaskProvider } from './task/CustomExecutionTerminal';
let client: LanguageClient; let client: LanguageClient;
let bfRunTaskProvider: Disposable;
export function activate(context: ExtensionContext) { export function activate(context: ExtensionContext) {
// The server is implemented in node // The server is implemented in node
let serverModule = context.asAbsolutePath( let serverModule = context.asAbsolutePath(
@@ -47,10 +51,13 @@ export function activate(context: ExtensionContext) {
commands.registerCommand( commands.registerCommand(
branFlakesCommand.getCommandName(), branFlakesCommand.getCommandName(),
branFlakesCommand.getCommandHandler() branFlakesCommand.getCommandHandler()
) ),
); );
} }
// Create the language client and start the client. // Create the language client and start the client.
client = new LanguageClient( client = new LanguageClient(
'brainfucklanguageserver', 'brainfucklanguageserver',
@@ -61,11 +68,17 @@ export function activate(context: ExtensionContext) {
// Start the client. This will also launch the server // Start the client. This will also launch the server
client.start(); client.start();
const workspaceRoot = (workspace.workspaceFolders && (workspace.workspaceFolders.length > 0))
? workspace.workspaceFolders[0].uri.fsPath : undefined;
bfRunTaskProvider = tasks.registerTaskProvider(CustomExecutionTaskProvider.type, new CustomExecutionTaskProvider(workspaceRoot));
} }
export function deactivate(): Thenable<void> | undefined { export function deactivate(): Thenable<void> | undefined {
if (!client) { if (!client) {
return undefined; return undefined;
} }
return client.stop(); bfRunTaskProvider?.dispose();
client?.stop();
} }

View File

@@ -1,4 +1,4 @@
export default interface InputStrategy { export default interface InputStrategy {
getInput(): Promise<number>; getInput(): Promise<number>;
} };

View File

@@ -1,5 +1,5 @@
import { CancellationToken, InputBoxOptions } from 'vscode'; import type { CancellationToken, InputBoxOptions } from 'vscode';
import InputStrategy from './InputStrategy'; import type InputStrategy from './InputStrategy';
export class VSCodePromptInputStrategy implements InputStrategy { export class VSCodePromptInputStrategy implements InputStrategy {
private inputQueue: string = ''; private inputQueue: string = '';
@@ -11,14 +11,19 @@ export class VSCodePromptInputStrategy implements InputStrategy {
) { } ) { }
async getInput(): Promise<number> { async getInput(): Promise<number> {
while (this.inputQueue.length == 0) { while (this.inputQueue.length === 0) {
await this.requestInputFromPrompt(); await this.requestInputFromPrompt();
} }
const character = this.inputQueue.charCodeAt(0); const character = this.popInputFromQueue();
this.inputQueue = this.inputQueue.substring(1);
return character; return character;
} }
private popInputFromQueue() {
const character = this.inputQueue.charCodeAt(0);
this.inputQueue = this.inputQueue.substring(1);
return character;
}
private async requestInputFromPrompt() { private async requestInputFromPrompt() {
const inputPrompt = await this.requestor({ const inputPrompt = await this.requestor({
prompt: 'More input is required. Please provide input:', prompt: 'More input is required. Please provide input:',

View File

@@ -0,0 +1,152 @@
import * as vscode from 'vscode';
import * as path from 'path';
import { BranFlakesExecutorVisitor } from '../exec/BranFlakesExecutorVisitor';
import { AbortClassRequestor } from '../exec/AbortRequestor';
interface BFRunTaskDefinition extends vscode.TaskDefinition {
file?: string;
}
export class CustomExecutionTaskProvider implements vscode.TaskProvider {
static type: string = 'bf-run';
tasks: vscode.Task[] | undefined;
constructor(private workspaceRoot: string | undefined) {
}
provideTasks(token?: vscode.CancellationToken): vscode.ProviderResult<vscode.Task[]> {
if (this.tasks !== undefined) { return this.tasks; }
this.tasks = [this.getTaskFromDefinition(undefined)];
return this.tasks;
}
getTaskFromDefinition(fileName: string | undefined): vscode.Task {
const definition: BFRunTaskDefinition = {
type: CustomExecutionTaskProvider.type,
file: fileName
};
return new vscode.Task(definition, vscode.TaskScope.Workspace, `bf: run: current file`, CustomExecutionTaskProvider.type,
new vscode.CustomExecution(async () => {
return new CustomBuildTaskTerminal(definition.file);
})
);
}
resolveTask(_task: vscode.Task, token?: vscode.CancellationToken): vscode.ProviderResult<vscode.Task> {
const definition: BFRunTaskDefinition = <any>_task.definition;
const fileNameRecovered = definition.file;
const taskName = `bf: run: ` + (fileNameRecovered ?? 'current file');
return new vscode.Task(definition, vscode.TaskScope.Workspace, taskName, CustomExecutionTaskProvider.type,
new vscode.CustomExecution(async () => {
return new CustomBuildTaskTerminal(definition.file);
})
);
}
}
function replaceLFWithCRLF(data: string) {
return data.replace(/(?<!\r)\n/gm, '\r\n');
}
class CustomBuildTaskTerminal implements vscode.Pseudoterminal {
private writeEmitter = new vscode.EventEmitter<string>();
private closeEmitter = new vscode.EventEmitter<number>();
private readEmitter = new vscode.EventEmitter<void>();
inputQueue: number[] = [];
private openDocument: vscode.TextDocument | undefined;
onDidWrite: vscode.Event<string> = this.writeEmitter.event;
onDidClose?: vscode.Event<number> = this.closeEmitter.event;
abortRequestor = new AbortClassRequestor();
handleInput(data: string): void {
// this.writeEmitter.fire(`Echo(${data.length})` + data);
const newData = [...data].map(e => e.charCodeAt(0));
console.log('new input', newData);
this.inputQueue.push(...newData);
this.writeEmitter.fire(replaceLFWithCRLF(data));
this.readEmitter.fire();
}
constructor(private fileName?: string) {
}
open(_initialDimensions: vscode.TerminalDimensions | undefined): void {
// At this point we can start using the terminal.
this.openDocumentForTask().then(this.doExecution.bind(this));
}
getPath(fileLocationString: string | undefined, fileName: string) {
if (fileLocationString === undefined) { return vscode.Uri.file(fileName); }
return vscode.Uri.file(path.resolve(fileLocationString, fileName));
}
private async openDocumentForTask() {
let openDocument: vscode.TextDocument;
if (this.fileName !== undefined) {
try {
const fileLocationPathString = vscode.workspace.workspaceFolders?.[0]?.uri?.fsPath;
const finalPath = this.getPath(fileLocationPathString, this.fileName);
openDocument = await vscode.workspace.openTextDocument(finalPath);
} catch (e) {
vscode.window.showErrorMessage('Failed to open file ' + this.fileName);
this.closeEmitter.fire(2);
}
} else {
openDocument = vscode.window.activeTextEditor.document;
}
this.openDocument = openDocument;
}
close(): void {
// The terminal has been closed. Shutdown the build.
console.log('Forced close');
this.abortRequestor.requestAbort();
}
private async doExecution(): Promise<void> {
this.writeEmitter.fire('[bf] Requested execution of ' + (this.fileName ?? 'active file') + '\r\n');
const cus = this;
try {
await BranFlakesExecutorVisitor.run(this.openDocument.getText(),
this.openDocument.uri.fsPath,
{
getInput() {
return new Promise((res, rej) => {
if (cus.inputQueue.length > 0) {
const char = cus.inputQueue.shift();
res(char);
} else {
const dispose: vscode.Disposable[] = [];
cus.readEmitter.event(e => {
const char = cus.inputQueue.shift();
//clear the earliest disposable
dispose.shift()?.dispose();
res(char);
}, null, dispose);
}
});
},
},
async (data) => {
this.writeEmitter.fire(replaceLFWithCRLF(data));
},this.abortRequestor
);
this.closeEmitter.fire(0);
} catch (e) {
this.closeEmitter.fire(1);
}
}
}

View File

@@ -16,7 +16,8 @@ const config = {
path: path.resolve(__dirname, 'dist'), path: path.resolve(__dirname, 'dist'),
filename: '[name].js', filename: '[name].js',
libraryTarget: 'commonjs2', libraryTarget: 'commonjs2',
devtoolModuleFilenameTemplate: '../[resource-path]' sourceMapFilename: '[name].js.map',
devtoolModuleFilenameTemplate: '../[resource-path]',
}, },
// devtool: 'source-map', // devtool: 'source-map',
externals: { externals: {
@@ -27,6 +28,7 @@ const config = {
innerGraph:true, innerGraph:true,
usedExports:true usedExports:true
}, },
resolve: { resolve: {
// support reading TypeScript and JavaScript files, 📖 -> https://github.com/TypeStrong/ts-loader // support reading TypeScript and JavaScript files, 📖 -> https://github.com/TypeStrong/ts-loader
extensions: ['.ts', '.js'], extensions: ['.ts', '.js'],

8085
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -5,9 +5,9 @@
"author": "Atreya Bain", "author": "Atreya Bain",
"license": "MIT", "license": "MIT",
"publisher": "atreyabain", "publisher": "atreyabain",
"version": "0.2.0", "version": "0.3.1",
"icon": "assets/128.png", "icon": "assets/128.png",
"categories": [], "categories": ["Programming Languages","Linters"],
"keywords": [ "keywords": [
"multi-root ready", "multi-root ready",
"brainfuck", "brainfuck",
@@ -29,7 +29,7 @@
"type": "git", "type": "git",
"url": "https://github.com/chrisvrose/bf-server" "url": "https://github.com/chrisvrose/bf-server"
}, },
"main": "./client/dist/extension", "main": "./client/dist/extension.js",
"contributes": { "contributes": {
"languages": [ "languages": [
{ {
@@ -57,7 +57,7 @@
], ],
"commands": [ "commands": [
{ {
"command": "bf.execute", "command": "bf.execute.old",
"title": "BF: Execute", "title": "BF: Execute",
"when": "editorLangId == bf", "when": "editorLangId == bf",
"enablement": "editorLangId == bf" "enablement": "editorLangId == bf"
@@ -85,7 +85,18 @@
"description": "Traces the communication between VS Code and the language server." "description": "Traces the communication between VS Code and the language server."
} }
} }
},
"taskDefinitions": [
{
"type": "bf-run",
"properties": {
"file":{
"type":"string",
"description": "The BF file to be executed. Can be omitted to run current file"
} }
}
}
]
}, },
"scripts": { "scripts": {
"vscode:prepublish": "npm run compile", "vscode:prepublish": "npm run compile",

128
server/src/connection.ts Normal file
View File

@@ -0,0 +1,128 @@
import { CompletionItem, CompletionItemKind, Connection, DidChangeConfigurationNotification, DidChangeConfigurationParams, InitializeParams, InitializeResult, TextDocumentPositionParams, TextDocuments, TextDocumentSyncKind } from 'vscode-languageserver';
import { validateTextDocument } from './validation';
import { TextDocument } from 'vscode-languageserver-textdocument';
import { BranFlakesSettings, defaultSettings, SettingsManager } from './settings';
export class BranFlakesConnectionManager {
constructor(protected connection: Connection, private validator:typeof validateTextDocument, private documents:TextDocuments<TextDocument>, private settingsManager:SettingsManager) {
connection.onInitialize(this.initConnection.bind(this));
connection.onDidChangeConfiguration(this.onDidChangeConfiguration.bind(this));
}
initConnection(params: InitializeParams) {
let capabilities = params.capabilities;
// Does the client support the `workspace/configuration` request?
// If not, we will fall back using global settings
this.settingsManager.hasConfigurationCapability = !!(
capabilities.workspace && !!capabilities.workspace.configuration
);
this.settingsManager.hasWorkspaceFolderCapability = !!(
capabilities.workspace && !!capabilities.workspace.workspaceFolders
);
this.settingsManager.hasDiagnosticRelatedInformationCapability = !!(
capabilities.textDocument &&
capabilities.textDocument.publishDiagnostics &&
capabilities.textDocument.publishDiagnostics.relatedInformation
);
const result: InitializeResult = {
capabilities: {
textDocumentSync: TextDocumentSyncKind.Incremental,
// Tell the client that the server supports code completion
completionProvider: {
resolveProvider: false,
triggerCharacters: ['.'],
},
},
};
if (this.settingsManager.hasWorkspaceFolderCapability) {
result.capabilities.workspace = {
workspaceFolders: {
supported: true,
},
};
}
return result;
}
onDidChangeConfiguration(change:DidChangeConfigurationParams){
if (this.settingsManager.hasConfigurationCapability) {
// Reset all cached document settings
this.settingsManager.clearDocumentSettings();
} else {
this.settingsManager.updateSettings(
(change.settings.languageServerExample || defaultSettings)
);
}
// Revalidate all open text documents
Promise.all(this.documents.all().map(validateTextDocument)).catch(e => {
this.connection.console.log('Failed to validate text documents');
});
}
onInit() {
if (this.settingsManager.hasConfigurationCapability) {
// Register for all configuration changes.
this.connection.client.register(
DidChangeConfigurationNotification.type,
undefined
);
}
if (this.settingsManager.hasWorkspaceFolderCapability) {
this.connection.workspace.onDidChangeWorkspaceFolders(_event => {
// connection.console.log('Workspace folder change event received.');
});
}
}
setOnCompletion() {
this.connection.onCompletion((_textDocumentPosition: TextDocumentPositionParams): CompletionItem[] => {
const completions: CompletionItem[] = [
{
label: '+',
detail: 'Addition',
documentation: 'Add one to cell',
},
{
label: '-',
detail: 'Subtraction',
documentation: 'Subtract one from cell',
},
{
label: ',',
detail: 'Input',
documentation: 'Ask for input (Stored in the ASCII format)',
},
{
label: '.',
detail: 'Output',
documentation: 'Output the equivalent ASCII character',
},
{
label: '>',
detail: 'Right Shift',
documentation: 'Shift the pointer one cell to the right',
},
{
label: '<',
detail: 'Left Shift',
documentation: 'Shift the pointer one cell to the Left',
},
];
return completions.map(e => {
e.kind = CompletionItemKind.Operator;
return e;
});
});
}
listen() {
this.connection.listen();
}
}

View File

@@ -5,119 +5,33 @@
import { import {
createConnection, createConnection,
TextDocuments, TextDocuments,
Diagnostic,
DiagnosticSeverity,
ProposedFeatures, ProposedFeatures,
InitializeParams,
DidChangeConfigurationNotification,
CompletionItem,
CompletionItemKind,
TextDocumentPositionParams,
TextDocumentSyncKind,
InitializeResult,
Position,
} from 'vscode-languageserver'; } from 'vscode-languageserver';
import { TextDocument } from 'vscode-languageserver-textdocument'; import { TextDocument } from 'vscode-languageserver-textdocument';
import { BranFlakesSettings, defaultSettings, SettingsManager } from './settings';
import { BranFlakesConnectionManager } from './connection';
import { validateTextDocument } from './validation';
// Create a connection for the server. The connection uses Node's IPC as a transport. // Create a connection for the server. The connection uses Node's IPC as a transport.
// Also include all preview / proposed LSP features. // Also include all preview / proposed LSP features.
let connection = createConnection(ProposedFeatures.all); export let connection = createConnection(ProposedFeatures.all);
// Create a simple text document manager. The text document manager // Create a simple text document manager. The text document manager
// supports full document sync only // supports full document sync only
let documents: TextDocuments<TextDocument> = new TextDocuments(TextDocument); let documents: TextDocuments<TextDocument> = new TextDocuments(TextDocument);
let hasConfigurationCapability: boolean = false;
let hasWorkspaceFolderCapability: boolean = false;
let hasDiagnosticRelatedInformationCapability: boolean = false;
connection.onInitialize((params: InitializeParams) => { let globalSettings: BranFlakesSettings = defaultSettings;
let capabilities = params.capabilities;
// Does the client support the `workspace/configuration` request?
// If not, we will fall back using global settings
hasConfigurationCapability = !!(
capabilities.workspace && !!capabilities.workspace.configuration
);
hasWorkspaceFolderCapability = !!(
capabilities.workspace && !!capabilities.workspace.workspaceFolders
);
hasDiagnosticRelatedInformationCapability = !!(
capabilities.textDocument &&
capabilities.textDocument.publishDiagnostics &&
capabilities.textDocument.publishDiagnostics.relatedInformation
);
const result: InitializeResult = {
capabilities: {
textDocumentSync: TextDocumentSyncKind.Incremental,
// Tell the client that the server supports code completion
completionProvider: {
resolveProvider: false,
triggerCharacters: ['.'],
},
},
};
if (hasWorkspaceFolderCapability) {
result.capabilities.workspace = {
workspaceFolders: {
supported: true,
},
};
}
return result;
});
connection.onInitialized(() => {
if (hasConfigurationCapability) {
// Register for all configuration changes.
connection.client.register(
DidChangeConfigurationNotification.type,
undefined
);
}
if (hasWorkspaceFolderCapability) {
connection.workspace.onDidChangeWorkspaceFolders(_event => {
connection.console.log('Workspace folder change event received.');
});
}
});
// The example settings
interface ExampleSettings {
maxNumberOfProblems: number;
}
// The global settings, used when the `workspace/configuration` request is not supported by the client.
// Please note that this is not the case when using this server with the client provided in this example
// but could happen with other clients.
const defaultSettings: ExampleSettings = { maxNumberOfProblems: 5 };
let globalSettings: ExampleSettings = defaultSettings;
// Cache the settings of all open documents // Cache the settings of all open documents
let documentSettings: Map<string, Thenable<ExampleSettings>> = new Map(); let documentSettings: Map<string, Thenable<BranFlakesSettings>> = new Map();
let settingsManager = new SettingsManager();
let cm = new BranFlakesConnectionManager(connection, validateTextDocument,documents,settingsManager);
connection.onDidChangeConfiguration(change => {
if (hasConfigurationCapability) {
// Reset all cached document settings
documentSettings.clear();
} else {
globalSettings = <ExampleSettings>(
(change.settings.languageServerExample || defaultSettings)
);
}
// Revalidate all open text documents export function getDocumentSettings(resource: string): Thenable<BranFlakesSettings> {
documents.all().forEach(validateTextDocument); if (!settingsManager.hasConfigurationCapability) {
});
function getDocumentSettings(resource: string): Thenable<ExampleSettings> {
if (!hasConfigurationCapability) {
return Promise.resolve(globalSettings); return Promise.resolve(globalSettings);
} }
let result = documentSettings.get(resource); let result = documentSettings.get(resource);
@@ -133,13 +47,12 @@ function getDocumentSettings(resource: string): Thenable<ExampleSettings> {
// Only keep settings for open documents // Only keep settings for open documents
documents.onDidClose(e => { documents.onDidClose(e => {
documentSettings.delete(e.document.uri); settingsManager.closeDocument(e.document.uri);
}); });
// The content of a text document has changed. This event is emitted // The content of a text document has changed. This event is emitted
// when the text document first opened or when its content has changed. // when the text document first opened or when its content has changed.
documents.onDidChangeContent(change => { documents.onDidChangeContent(change => {
//change.contentChanges;
validateTextDocument(change.document); validateTextDocument(change.document);
}); });
@@ -148,112 +61,12 @@ documents.onDidSave(change => {
validateTextDocument(change.document); validateTextDocument(change.document);
}); });
const validateBrackets = (text: string) => {
let count = 0, lp: number[] = [],issues:number[]=[];
const textsplit = text.split('');
textsplit.forEach((x, i) => {
if (x === '[' || x === ']') {
if (x === '[') {lp.push(i);}
if (x === ']') {if(lp.length===0) {issues.push(i);}lp.pop();}
}
});
return [...lp,...issues];
};
async function validateTextDocument(textDocument: TextDocument): Promise<void> {
// In this simple example we get the settings for every validate run.
let settings = await getDocumentSettings(textDocument.uri);
// The validator creates diagnostics for all uppercase words length 2 and more
const text = textDocument.getText();
let problems = 0;
let diagnostics: Diagnostic[] = [];
const issues = validateBrackets(text);
diagnostics.push(...issues.map<Diagnostic>(e => ({
message: 'Brackets unmatched',
range:{
start: textDocument.positionAt(e),
end: textDocument.positionAt(e+1),
},
severity:DiagnosticSeverity.Error,
code:'[ and ]',
})));
// diagnostics.push({
// message: 'Brackets not matched',
// range: {
// start: { line: 0, character: 0 },
// end: { line: 0, character: 0 },
// },
// });
// diagnostics.push(<Diagnostic>{
// severity: DiagnosticSeverity.Information,
// range: {
// start: textDocument.positionAt(0),
// end: textDocument.positionAt(1),
// },
// // message:`HI:(${text})(${result.hasErrors()},${result.hasWarnings()})`,
// message: `Parsing Failed`,
// source: 'test',
// });
// Send the computed diagnostics to VSCode.
connection.sendDiagnostics({ uri: textDocument.uri, diagnostics });
}
// This handler provides the initial list of the completion items. // This handler provides the initial list of the completion items.
connection.onCompletion( cm.setOnCompletion();
(_textDocumentPosition: TextDocumentPositionParams): CompletionItem[] => {
const completions: CompletionItem[] = [
{
label: '+',
detail: 'Addition',
documentation: 'Add one to cell',
},
{
label: '-',
detail: 'Subtraction',
documentation: 'Subtract one from cell',
},
{
label: ',',
detail: 'Input',
documentation: 'Ask for input (Stored in the ASCII format)',
},
{
label: '.',
detail: 'Output',
documentation: 'Output the equivalent ASCII character',
},
{
label: '>',
detail: 'Right Shift',
documentation: 'Shift the pointer one cell to the right',
},
{
label: '<',
detail: 'Left Shift',
documentation: 'Shift the pointer one cell to the Left',
},
];
return completions.map(e => {
e.kind = CompletionItemKind.Operator;
return e;
});
}
);
// Make the text document manager listen on the connection // Make the text document manager listen on the connection
// for open, change and close text document events // for open, change and close text document events
documents.listen(connection); documents.listen(connection);
// Listen on the connection // Listen on the connection
connection.listen(); cm.listen();

35
server/src/settings.ts Normal file
View File

@@ -0,0 +1,35 @@
// The example settings
export interface BranFlakesSettings {
maxNumberOfProblems: number;
}
// The global settings, used when the `workspace/configuration` request is not supported by the client.
// Please note that this is not the case when using this server with the client provided in this example
// but could happen with other clients.
export const defaultSettings: BranFlakesSettings = {
maxNumberOfProblems: 5
};
export class SettingsManager {
hasConfigurationCapability: boolean = false;
hasWorkspaceFolderCapability: boolean = false;
hasDiagnosticRelatedInformationCapability: boolean = false;
documentSettings: Map<string, Thenable<BranFlakesSettings>> = new Map();
#settings = defaultSettings;
updateSettings(newSettings: BranFlakesSettings) {
this.#settings = newSettings;
}
closeDocument(doc: string) {
this.documentSettings.delete(doc);
}
clearDocumentSettings(){
this.documentSettings.clear();
}
}

54
server/src/validation.ts Normal file
View File

@@ -0,0 +1,54 @@
import { Diagnostic, DiagnosticSeverity } from 'vscode-languageserver';
import { TextDocument } from 'vscode-languageserver-textdocument';
import { getDocumentSettings, connection } from './server';
export const validateBrackets = (text: string) => {
let count = 0, lp: number[] = [], issues: number[] = [];
const textsplit = text.split('');
textsplit.forEach((x, i) => {
if (x === '[' || x === ']') {
if (x === '[') {
lp.push(i);
}
if (x === ']') {
if (lp.length === 0) {
issues.push(i);
}
lp.pop();
}
}
});
return [...lp, ...issues];
};
export async function validateTextDocument(textDocument: TextDocument): Promise<void> {
// In this simple example we get the settings for every validate run.
let settings = await getDocumentSettings(textDocument.uri);
// The validator creates diagnostics for all uppercase words length 2 and more
const text = textDocument.getText();
let problems = 0;
let diagnostics: Diagnostic[] = [];
const issues = validateBrackets(text);
diagnostics.push(
...issues.map<Diagnostic>(e => ({
message: 'Brackets unmatched',
range: {
start: textDocument.positionAt(e),
end: textDocument.positionAt(e + 1),
},
severity: DiagnosticSeverity.Error,
code: '[ and ]',
}))
);
// Send the computed diagnostics to VSCode.
connection.sendDiagnostics({ uri: textDocument.uri, diagnostics });
}

View File

@@ -5,7 +5,8 @@
"lib": ["ES2019"], "lib": ["ES2019"],
"outDir": "out", "outDir": "out",
"rootDir": "src", "rootDir": "src",
"sourceMap": true "sourceMap": true,
"strict": true
}, },
"include": [ "include": [
"src" "src"