diff --git a/src/editor.ts b/src/editor.ts index 931b6e6..fb5af76 100644 --- a/src/editor.ts +++ b/src/editor.ts @@ -80,19 +80,17 @@ export class TidalEditor { public getTidalExpressionUnderCursor(getMultiline: boolean): TidalExpression | null { const document = this.editor.document; - const position = this.editor.selection.active; - - const line = document.lineAt(position); - - // If there is a single-line expression - // TODO: decide the behaviour in case in multi-line selections - if (!getMultiline) { - if (this.isEmpty(document, position.line)) { return null; } - let range = new Range(line.lineNumber, 0, line.lineNumber, line.text.length); - return new TidalExpression(line.text, range); + const startLine = document.lineAt(this.editor.selection.start); + const endLine = document.lineAt(this.editor.selection.end); + + // If there is a single-line expression or selection + if (!getMultiline || startLine.lineNumber !== endLine.lineNumber) { + const range = new Range(startLine.lineNumber, 0, endLine.lineNumber, endLine.text.length); + const text = document.getText(range); + if (text.trim().length === 0) { return null; } + return new TidalExpression(text, range); } - - // If there is a multi-line expression + // If there is a multi-line expression without selection const selectedRange = new Range(this.editor.selection.anchor, this.editor.selection.active); const startLineNumber = this.getStartLineNumber(document, selectedRange); if (startLineNumber === null) { @@ -102,7 +100,7 @@ export class TidalEditor { const endLineNumber = this.getEndLineNumber(document, startLineNumber); const endCol = document.lineAt(endLineNumber).text.length; - let range = new Range(startLineNumber, 0, endLineNumber, endCol); + const range = new Range(startLineNumber, 0, endLineNumber, endCol); return new TidalExpression(document.getText(range), range); } diff --git a/src/repl.ts b/src/repl.ts index 407ceef..af48c23 100644 --- a/src/repl.ts +++ b/src/repl.ts @@ -31,7 +31,7 @@ export class Repl implements IRepl { return; } - await this.tidal.sendTidalExpression('hush'); + await this.tidal.sendTidalExpression('hush', false); this.history.log(new TidalExpression('hush', new vscode.Range(0, 0, 0, 0))); } @@ -43,7 +43,7 @@ export class Repl implements IRepl { const block = new TidalEditor(this.textEditor).getTidalExpressionUnderCursor(isMultiline); if (block) { - await this.tidal.sendTidalExpression(block.expression); + await this.tidal.sendTidalExpression(block.expression, isMultiline); this.feedback(block.range); this.history.log(block); } diff --git a/src/tidal.ts b/src/tidal.ts index 3da9bc3..f397055 100644 --- a/src/tidal.ts +++ b/src/tidal.ts @@ -7,7 +7,7 @@ import * as vscode from 'vscode'; * Provides an interface to send instructions to the current Tidal instance. */ export interface ITidal { - sendTidalExpression(expression: string): Promise; + sendTidalExpression(expression: string, isMultilineStatement: boolean): Promise; } export class Tidal implements ITidal { @@ -60,18 +60,65 @@ export class Tidal implements ITidal { return true; } - public async sendTidalExpression(expression: string) { + public async sendTidalExpression(expression: string, isMultilineStatement: boolean) { if (!await this.bootTidal()) { this.logger.error('Could not boot Tidal'); return; } - this.ghci.writeLn(':{'); const splits = expression.split(/[\r\n]+/); + // nothing to evaluate + if (splits.length === 0 || splits[0] === null) { return; } + + // directly write single line to ghci + if (splits.length === 1) { + this.ghci.writeLn(splits[0]); + return; + } + + // if user requested multiline eval using ctrl+enter + // wrap lines in multiline ghci-operator + // required for multiline statements without indentation like function declarations + if (isMultilineStatement) { + this.ghci.writeLn(":{"); + for (let i = 0; i < splits.length; i++) { + this.ghci.writeLn(splits[i]); + } + this.ghci.writeLn(":}"); + return; + } + + // if user requested single line eval of a selection using shift+enter + // keep track of current indent level and wrap in multi-line ops if required + let indent = splits[0].replace(/^(\s*).*$/,"$1").length; + let multilineOpen = false; // keep track of open multi-line operator + let multilineMustClose = false; // if op must be closed after writing current statement for (let i = 0; i < splits.length; i++) { - this.ghci.writeLn(splits[i]); + const currentSplit = splits[i] + if (i + 1 < splits.length) { + const futureIndent = splits[i + 1].replace(/^(\s*).*$/,"$1").length; + // next statement is deeper indented, guessing it to be part of current statement, opening + if (futureIndent > indent && !multilineOpen) { + multilineOpen = true; + this.ghci.writeLn(":{"); + // next statement is shallower indented, guessing statement is done, closing + } else if (futureIndent < indent && multilineOpen) { + multilineMustClose = true; + } + indent = futureIndent; + } + this.ghci.writeLn(currentSplit); + // close open op before next, shallower indented statement + if (multilineMustClose) { + multilineMustClose = false; + multilineOpen = false; + this.ghci.writeLn(":}") + } + } + // close remaining open op + if (multilineOpen) { + this.ghci.writeLn(":}"); } - this.ghci.writeLn(':}'); } private async getBootCommandsFromFile(uri: vscode.Uri): Promise { diff --git a/test/expression.test.ts b/test/expression.test.ts index abb3a00..6196b58 100644 --- a/test/expression.test.ts +++ b/test/expression.test.ts @@ -13,7 +13,7 @@ suite("Editor", () => { assert.isNotNull(expression); if (expression !== null) { - expect(expression.expression).to.be.equal("Hello world"); + expect(expression.expression).to.be.equal("Hello world\r\n"); } }); @@ -26,7 +26,7 @@ suite("Editor", () => { assert.isNotNull(expression); if (expression !== null) { - expect(expression.expression).to.be.equal("Hello world"); + expect(expression.expression).to.be.equal("Hello world\r\n"); } }); diff --git a/test/repl.test.ts b/test/repl.test.ts index c6eae68..a3745f7 100644 --- a/test/repl.test.ts +++ b/test/repl.test.ts @@ -22,7 +22,7 @@ suite('Repl', () => { mockConfig.object, mockCreateTextEditorDecorationType.object); await repl.hush(); - mockTidal.verify(t => t.sendTidalExpression('hush'), TypeMoq.Times.once()); + mockTidal.verify(t => t.sendTidalExpression('hush', false), TypeMoq.Times.once()); mockHistory.verify(h => h.log(TypeMoq.It.isAny()), TypeMoq.Times.once()); }); @@ -40,7 +40,7 @@ suite('Repl', () => { mockConfig.object, mockCreateTextEditorDecorationType.object); await repl.hush(); - mockTidal.verify(t => t.sendTidalExpression(TypeMoq.It.isAnyString()), TypeMoq.Times.never()); + mockTidal.verify(t => t.sendTidalExpression(TypeMoq.It.isAnyString(), false), TypeMoq.Times.never()); mockHistory.verify(h => h.log(TypeMoq.It.isAny()), TypeMoq.Times.never()); }); @@ -58,7 +58,7 @@ suite('Repl', () => { mockConfig.object, mockCreateTextEditorDecorationType.object); await repl.evaluate(false); - mockTidal.verify(t => t.sendTidalExpression(TypeMoq.It.isAnyString()), TypeMoq.Times.never()); + mockTidal.verify(t => t.sendTidalExpression(TypeMoq.It.isAnyString(), false), TypeMoq.Times.never()); mockHistory.verify(h => h.log(TypeMoq.It.isAny()), TypeMoq.Times.never()); }); @@ -76,7 +76,7 @@ suite('Repl', () => { mockConfig.object, mockCreateTextEditorDecorationType.object); await repl.evaluate(true); - mockTidal.verify(t => t.sendTidalExpression('Foo\r\nbar'), TypeMoq.Times.once()); + mockTidal.verify(t => t.sendTidalExpression('Foo\r\nbar', true), TypeMoq.Times.once()); mockHistory.verify(h => h.log(TypeMoq.It.isAny()), TypeMoq.Times.once()); }); @@ -94,7 +94,8 @@ suite('Repl', () => { mockConfig.object, mockCreateTextEditorDecorationType.object); await repl.evaluate(false); - mockTidal.verify(t => t.sendTidalExpression('bar'), TypeMoq.Times.once()); + mockTidal.verify(t => t.sendTidalExpression('bar', false), TypeMoq.Times.once()); mockHistory.verify(h => h.log(TypeMoq.It.isAny()), TypeMoq.Times.once()); }); + }); diff --git a/test/tidal.test.ts b/test/tidal.test.ts index 5111542..5450035 100644 --- a/test/tidal.test.ts +++ b/test/tidal.test.ts @@ -14,7 +14,7 @@ suite("Tidal", () => { mockedGhci.setup(ghci => ghci.writeLn('d1 $ sound "bd"')).verifiable(TypeMoq.Times.once()); mockedGhci.setup(ghci => ghci.writeLn(':}')).verifiable(TypeMoq.Times.once()); - return tidal.sendTidalExpression('d1 $ sound "bd"').then(() => { + return tidal.sendTidalExpression('d1 $ sound "bd"', false).then(() => { mockedGhci.verifyAll(); }); }); @@ -31,7 +31,7 @@ suite("Tidal", () => { mockedGhci.setup(ghci => ghci.writeLn('sound "bd"')).verifiable(TypeMoq.Times.once()); mockedGhci.setup(ghci => ghci.writeLn(':}')).verifiable(TypeMoq.Times.once()); - return tidal.sendTidalExpression(`d1 $${lineEnding}sound "bd"`).then(() => { + return tidal.sendTidalExpression(`d1 $${lineEnding}sound "bd"`, true).then(() => { mockedGhci.verifyAll(); }); });