From 214d1fec2cd794c4d777a4777fc6dbf6b6ae1c22 Mon Sep 17 00:00:00 2001 From: Guillaume Martres Date: Tue, 9 Oct 2018 17:56:40 +0200 Subject: [PATCH 1/3] vscode-dotty: typesafe messages This replaces the use of strings for identifying messages by types (WorksheetExecRequest and WorksheetPublishOutputNotification). Also contains some light cleanup/uniformization --- .../worksheet/WorksheetClient.scala | 8 ++-- .../worksheet/WorksheetMessages.scala | 17 +++---- .../worksheet/WorksheetService.scala | 6 +-- .../util/actions/WorksheetAction.scala | 4 +- vscode-dotty/src/extension.ts | 5 +- vscode-dotty/src/protocol.ts | 47 +++++++++++++++++++ vscode-dotty/src/worksheet.ts | 31 +++--------- 7 files changed, 72 insertions(+), 46 deletions(-) create mode 100644 vscode-dotty/src/protocol.ts diff --git a/language-server/src/dotty/tools/languageserver/worksheet/WorksheetClient.scala b/language-server/src/dotty/tools/languageserver/worksheet/WorksheetClient.scala index d0ee71670162..aa43d7df49ee 100644 --- a/language-server/src/dotty/tools/languageserver/worksheet/WorksheetClient.scala +++ b/language-server/src/dotty/tools/languageserver/worksheet/WorksheetClient.scala @@ -4,11 +4,13 @@ import org.eclipse.lsp4j.services.LanguageClient import org.eclipse.lsp4j.jsonrpc.services.JsonNotification /** - * A `LanguageClient` that supports the `worksheet/publishOutput` notification. - * - * @see dotty.tools.languageserver.worksheet.WorksheetExecOutput + * A `LanguageClient` that supports worksheet-specific notifications. */ trait WorksheetClient extends LanguageClient { + /** + * A notification that tells the client that a line of a worksheet produced + * the specified output. + */ @JsonNotification("worksheet/publishOutput") def publishOutput(output: WorksheetExecOutput): Unit } diff --git a/language-server/src/dotty/tools/languageserver/worksheet/WorksheetMessages.scala b/language-server/src/dotty/tools/languageserver/worksheet/WorksheetMessages.scala index 6ddb72903573..05dcf27e8c31 100644 --- a/language-server/src/dotty/tools/languageserver/worksheet/WorksheetMessages.scala +++ b/language-server/src/dotty/tools/languageserver/worksheet/WorksheetMessages.scala @@ -2,26 +2,21 @@ package dotty.tools.languageserver.worksheet import org.eclipse.lsp4j.VersionedTextDocumentIdentifier +// All case classes in this file should have zero-parameters secondary +// constructors to allow Gson to reflectively create instances on +// deserialization without relying on sun.misc.Unsafe. + /** The parameter for the `worksheet/exec` request. */ case class WorksheetExecParams(textDocument: VersionedTextDocumentIdentifier) { - // Used for deserialization - // see https://github.com/lampepfl/dotty/pull/5102#discussion_r222055355 def this() = this(null) } /** The response to a `worksheet/exec` request. */ -case class WorksheetExecResponse(success: Boolean) { - // Used for deserialization - // see https://github.com/lampepfl/dotty/pull/5102#discussion_r222055355 +case class WorksheetExecResult(success: Boolean) { def this() = this(false) } -/** - * A notification that tells the client that a line of a worksheet - * produced the specified output. - */ +/** The parameters to the `worksheet/publishOutput` notification. */ case class WorksheetExecOutput(textDocument: VersionedTextDocumentIdentifier, line: Int, content: String) { - // Used for deserialization - // see https://github.com/lampepfl/dotty/pull/5102#discussion_r222055355 def this() = this(null, 0, null) } diff --git a/language-server/src/dotty/tools/languageserver/worksheet/WorksheetService.scala b/language-server/src/dotty/tools/languageserver/worksheet/WorksheetService.scala index 2436d834784c..c8cf1b277a64 100644 --- a/language-server/src/dotty/tools/languageserver/worksheet/WorksheetService.scala +++ b/language-server/src/dotty/tools/languageserver/worksheet/WorksheetService.scala @@ -16,7 +16,7 @@ trait WorksheetService { thisServer: DottyLanguageServer => val worksheets: ConcurrentHashMap[URI, CompletableFuture[_]] = new ConcurrentHashMap() @JsonRequest - def exec(params: WorksheetExecParams): CompletableFuture[WorksheetExecResponse] = thisServer.synchronized { + def exec(params: WorksheetExecParams): CompletableFuture[WorksheetExecResult] = thisServer.synchronized { val uri = new URI(params.textDocument.getUri) val future = computeAsync { cancelChecker => @@ -24,10 +24,10 @@ trait WorksheetService { thisServer: DottyLanguageServer => val driver = driverFor(uri) val sendMessage = (line: Int, msg: String) => client.publishOutput(WorksheetExecOutput(params.textDocument, line, msg)) evaluateWorksheet(driver, uri, sendMessage, cancelChecker)(driver.currentCtx) - WorksheetExecResponse(success = true) + WorksheetExecResult(success = true) } catch { case _: Throwable => - WorksheetExecResponse(success = false) + WorksheetExecResult(success = false) } finally { worksheets.remove(uri) } diff --git a/language-server/test/dotty/tools/languageserver/util/actions/WorksheetAction.scala b/language-server/test/dotty/tools/languageserver/util/actions/WorksheetAction.scala index a60be99b7e0d..78a890345fe4 100644 --- a/language-server/test/dotty/tools/languageserver/util/actions/WorksheetAction.scala +++ b/language-server/test/dotty/tools/languageserver/util/actions/WorksheetAction.scala @@ -1,6 +1,6 @@ package dotty.tools.languageserver.util.actions -import dotty.tools.languageserver.worksheet.{WorksheetExecOutput, WorksheetExecParams, WorksheetExecResponse} +import dotty.tools.languageserver.worksheet.{WorksheetExecOutput, WorksheetExecParams, WorksheetExecResult} import dotty.tools.languageserver.util.embedded.CodeMarker import java.net.URI @@ -11,7 +11,7 @@ import org.eclipse.lsp4j.VersionedTextDocumentIdentifier abstract class WorksheetAction extends Action { /** Triggers the evaluation of the worksheet. */ - def triggerEvaluation(marker: CodeMarker): Exec[CompletableFuture[WorksheetExecResponse]] = { + def triggerEvaluation(marker: CodeMarker): Exec[CompletableFuture[WorksheetExecResult]] = { server.exec(WorksheetExecParams(marker.toVersionedTextDocumentIdentifier)) } diff --git a/vscode-dotty/src/extension.ts b/vscode-dotty/src/extension.ts index e9c25ad5ec2c..c5a762d54441 100644 --- a/vscode-dotty/src/extension.ts +++ b/vscode-dotty/src/extension.ts @@ -11,9 +11,10 @@ import * as vscode from 'vscode'; import { LanguageClient, LanguageClientOptions, RevealOutputChannelOn, ServerOptions } from 'vscode-languageclient'; import { enableOldServerWorkaround } from './compat' - +import { WorksheetPublishOutputNotification } from './protocol' import * as worksheet from './worksheet' + let extensionContext: ExtensionContext let outputChannel: vscode.OutputChannel export let client: LanguageClient @@ -206,7 +207,7 @@ function run(serverOptions: ServerOptions, isOldServer: boolean) { enableOldServerWorkaround(client) client.onReady().then(() => { - client.onNotification("worksheet/publishOutput", (params) => { + client.onNotification(WorksheetPublishOutputNotification.type, params => { worksheet.handleMessage(params) }) }) diff --git a/vscode-dotty/src/protocol.ts b/vscode-dotty/src/protocol.ts new file mode 100644 index 000000000000..4719c6b283d1 --- /dev/null +++ b/vscode-dotty/src/protocol.ts @@ -0,0 +1,47 @@ +import * as vscode from 'vscode' +import { RequestType, NotificationType } from 'vscode-jsonrpc' +import { VersionedTextDocumentIdentifier } from 'vscode-languageserver-protocol' + +import { client } from './extension' + +/** The parameters for the `worksheet/exec` request. */ +export interface WorksheetExecParams { + textDocument: VersionedTextDocumentIdentifier +} + +/** The result of the `worksheet/exec` request. */ +export interface WorksheetExecResult { + success: boolean +} + +/** The parameters for the `worksheet/publishOutput` notification. */ +export interface WorksheetPublishOutputParams { + textDocument: VersionedTextDocumentIdentifier + line: number + content: string +} + +// TODO: Can be removed once https://github.com/Microsoft/vscode-languageserver-node/pull/421 +// is merged. +export function asVersionedTextDocumentIdentifier(textDocument: vscode.TextDocument): VersionedTextDocumentIdentifier { + return { + uri: client.code2ProtocolConverter.asUri(textDocument.uri), + version: textDocument.version + } +} + +export function asWorksheetExecParams(textDocument: vscode.TextDocument): WorksheetExecParams { + return { + textDocument: asVersionedTextDocumentIdentifier(textDocument) + } +} + +/** The `worksheet/exec` request */ +export namespace WorksheetExecRequest { + export const type = new RequestType("worksheet/exec") +} + +/** The `worksheet/publishOutput` notification */ +export namespace WorksheetPublishOutputNotification { + export const type = new NotificationType("worksheet/publishOutput") +} diff --git a/vscode-dotty/src/worksheet.ts b/vscode-dotty/src/worksheet.ts index d6b972cebbdc..239e9b1ea28f 100644 --- a/vscode-dotty/src/worksheet.ts +++ b/vscode-dotty/src/worksheet.ts @@ -1,6 +1,9 @@ import * as vscode from 'vscode' + import { client } from './extension' -import { VersionedTextDocumentIdentifier } from 'vscode-languageserver-protocol' +import { + asWorksheetExecParams, WorksheetExecRequest, WorksheetExecParams, WorksheetPublishOutputParams +} from './protocol' /** A worksheet managed by vscode */ class Worksheet { @@ -61,28 +64,6 @@ class Worksheet { } } -/** The parameter for the `worksheet/exec` request. */ -class WorksheetExecParams { - constructor(textDocument: vscode.TextDocument) { - this.textDocument = VersionedTextDocumentIdentifier.create(textDocument.uri.toString(), textDocument.version) - } - - readonly textDocument: VersionedTextDocumentIdentifier -} - -/** The parameter for the `worksheet/publishOutput` notification. */ -class WorksheetOutput { - constructor(textDocument: VersionedTextDocumentIdentifier, line: number, content: string) { - this.textDocument = textDocument - this.line = line - this.content = content - } - - readonly textDocument: VersionedTextDocumentIdentifier - readonly line: number - readonly content: string -} - /** * The command key for evaluating a worksheet. Exposed to users as * `Run worksheet`. @@ -139,7 +120,7 @@ export function evaluateWorksheet(document: vscode.TextDocument): Thenable<{}> { title: "Evaluating worksheet", cancellable: true }, (_, token) => { - return client.sendRequest("worksheet/exec", new WorksheetExecParams(worksheet.document), token) + return client.sendRequest(WorksheetExecRequest.type, asWorksheetExecParams(document), token) }) } else { return Promise.reject() @@ -173,7 +154,7 @@ function _prepareWorksheet(worksheet: Worksheet) { * * @param message The result of evaluating part of a worksheet. */ -export function handleMessage(output: WorksheetOutput) { +export function handleMessage(output: WorksheetPublishOutputParams) { const editor = vscode.window.visibleTextEditors.find(e => { let uri = e.document.uri.toString() From 0f465e52d5819628ceada3891d4e59780358e162 Mon Sep 17 00:00:00 2001 From: Guillaume Martres Date: Wed, 10 Oct 2018 14:59:25 +0200 Subject: [PATCH 2/3] Refactor worksheet support, make it a dynamic feature - Only enable worksheet features on the client if the server supports them using the LSP support for dynamic feature registration. - Replace the global functions and state in worksheet.ts by a WorksheetProvider class, move some of the global functions specific to one worksheet into Worksheet --- .../languageserver/DottyLanguageServer.scala | 4 +- vscode-dotty/package.json | 6 +- vscode-dotty/src/extension.ts | 29 +- vscode-dotty/src/features.ts | 62 +++ vscode-dotty/src/worksheet.ts | 504 +++++++++--------- 5 files changed, 324 insertions(+), 281 deletions(-) create mode 100644 vscode-dotty/src/features.ts diff --git a/language-server/src/dotty/tools/languageserver/DottyLanguageServer.scala b/language-server/src/dotty/tools/languageserver/DottyLanguageServer.scala index 63812f48ade4..d73fc002fc92 100644 --- a/language-server/src/dotty/tools/languageserver/DottyLanguageServer.scala +++ b/language-server/src/dotty/tools/languageserver/DottyLanguageServer.scala @@ -153,7 +153,9 @@ class DottyLanguageServer extends LanguageServer rootUri = params.getRootUri assert(rootUri != null) - val c = new ServerCapabilities + class DottyServerCapabilities(val worksheetRunProvider: Boolean = true) extends lsp4j.ServerCapabilities + + val c = new DottyServerCapabilities c.setTextDocumentSync(TextDocumentSyncKind.Full) c.setDocumentHighlightProvider(true) c.setDocumentSymbolProvider(true) diff --git a/vscode-dotty/package.json b/vscode-dotty/package.json index 682335ff5bbe..1ae1c4f83af5 100644 --- a/vscode-dotty/package.json +++ b/vscode-dotty/package.json @@ -43,11 +43,13 @@ "commands": [ { "command": "worksheet.evaluate", - "title": "Run worksheet" + "title": "Run worksheet", + "category": "Scala" }, { "command": "worksheet.cancel", - "title": "Cancel worksheet evaluation" + "title": "Cancel worksheet evaluation", + "category": "Scala" } ], "configurationDefaults": { diff --git a/vscode-dotty/src/extension.ts b/vscode-dotty/src/extension.ts index c5a762d54441..9eb6482c463f 100644 --- a/vscode-dotty/src/extension.ts +++ b/vscode-dotty/src/extension.ts @@ -13,11 +13,12 @@ import { LanguageClient, LanguageClientOptions, RevealOutputChannelOn, import { enableOldServerWorkaround } from './compat' import { WorksheetPublishOutputNotification } from './protocol' import * as worksheet from './worksheet' +import * as features from './features' +export let client: LanguageClient let extensionContext: ExtensionContext let outputChannel: vscode.OutputChannel -export let client: LanguageClient export function activate(context: ExtensionContext) { extensionContext = context @@ -31,18 +32,6 @@ export function activate(context: ExtensionContext) { const languageServerDefaultConfigFile = path.join(extensionContext.extensionPath, './out/default-dotty-ide-config') const coursierPath = path.join(extensionContext.extensionPath, './out/coursier'); - vscode.workspace.onWillSaveTextDocument(worksheet.prepareWorksheet) - vscode.workspace.onDidSaveTextDocument(document => { - if (worksheet.isWorksheet(document)) { - worksheet.evaluateWorksheet(document) - } - }) - vscode.workspace.onDidCloseTextDocument(document => { - if (worksheet.isWorksheet(document)) { - worksheet.removeWorksheet(document) - } - }) - if (process.env['DLS_DEV_MODE']) { const portFile = `${vscode.workspace.rootPath}/.dotty-ide-dev-port` fs.readFile(portFile, (err, port) => { @@ -203,20 +192,12 @@ function run(serverOptions: ServerOptions, isOldServer: boolean) { } client = new LanguageClient("dotty", "Dotty", serverOptions, clientOptions) + client.registerFeature(new features.WorksheetExecFeature(client)) + if (isOldServer) enableOldServerWorkaround(client) - client.onReady().then(() => { - client.onNotification(WorksheetPublishOutputNotification.type, params => { - worksheet.handleMessage(params) - }) - }) - - vscode.commands.registerCommand(worksheet.worksheetEvaluateKey, () => { - worksheet.evaluateWorksheetCommand() - }) - // Push the disposable to the context's subscriptions so that the // client can be deactivated on extension deactivation - extensionContext.subscriptions.push(client.start()); + extensionContext.subscriptions.push(client.start()) } diff --git a/vscode-dotty/src/features.ts b/vscode-dotty/src/features.ts new file mode 100644 index 000000000000..247758bf595c --- /dev/null +++ b/vscode-dotty/src/features.ts @@ -0,0 +1,62 @@ +import * as vscode from 'vscode' +import { + BaseLanguageClient, ClientCapabilities, DynamicFeature, ServerCapabilities, + TextDocumentFeature, TextDocumentRegistrationOptions, +} from 'vscode-languageclient' +import { generateUuid } from 'vscode-languageclient/lib/utils/uuid' +import { DocumentSelector } from 'vscode-languageserver-protocol' +import { Disposable } from 'vscode-jsonrpc' + +import { WorksheetExecRequest } from './protocol' +import { WorksheetProvider } from './worksheet' + +// Remove this if +// https://github.com/Microsoft/vscode-languageserver-node/issues/423 is fixed. +function ensure(target: T, key: K): T[K] { + if (target[key] === void 0) { + target[key] = {} as any; + } + return target[key]; +} + +export interface WorksheetClientCapabilities { + worksheet?: { + exec?: { + dynamicRegistration?: boolean + } + } +} + +export interface WorksheetServerCapabilities { + /** + * The server provides support for running worksheets. + */ + worksheetRunProvider?: boolean +} + +export class WorksheetExecFeature extends TextDocumentFeature { + constructor(client: BaseLanguageClient) { + super(client, WorksheetExecRequest.type) + } + + public fillClientCapabilities(capabilities: ClientCapabilities & WorksheetClientCapabilities): void { + ensure(ensure(capabilities, "worksheet")!, "exec")!.dynamicRegistration = true + } + + public initialize(capabilities: ServerCapabilities & WorksheetServerCapabilities, documentSelector: DocumentSelector): void { + if (!capabilities.worksheetRunProvider) { + return + } + + const selector: DocumentSelector = documentSelector || [ { language: 'scala', pattern: '**/*.sc' } ] + this.register(this.messages, { + id: generateUuid(), + registerOptions: { documentSelector: selector } + }) + } + + protected registerLanguageProvider(options: TextDocumentRegistrationOptions): Disposable { + let client = this._client + return new WorksheetProvider(client, options.documentSelector!) + } +} diff --git a/vscode-dotty/src/worksheet.ts b/vscode-dotty/src/worksheet.ts index 239e9b1ea28f..8db6b90c8941 100644 --- a/vscode-dotty/src/worksheet.ts +++ b/vscode-dotty/src/worksheet.ts @@ -1,316 +1,312 @@ import * as vscode from 'vscode' -import { client } from './extension' import { - asWorksheetExecParams, WorksheetExecRequest, WorksheetExecParams, WorksheetPublishOutputParams + asWorksheetExecParams, WorksheetExecRequest, WorksheetExecParams, + WorksheetPublishOutputParams, WorksheetPublishOutputNotification } from './protocol' +import { BaseLanguageClient, DocumentSelector } from 'vscode-languageclient' +import { Disposable } from 'vscode-jsonrpc' + +/** + * The command key for evaluating a worksheet. Exposed to users as + * `Run worksheet`. + */ +export const worksheetEvaluateKey = "worksheet.evaluate" /** A worksheet managed by vscode */ class Worksheet { - private constructor(document: vscode.TextDocument) { - this.document = document + constructor(readonly document: vscode.TextDocument, readonly client: BaseLanguageClient) { } - /** The text document that this worksheet represents. */ - readonly document: vscode.TextDocument - /** All decorations that have been added so far */ - decorationTypes: vscode.TextEditorDecorationType[] = [] + private decorationTypes: vscode.TextEditorDecorationType[] = [] /** The number of blank lines that have been inserted to fit the output so far. */ - insertedLines: number = 0 + private insertedLines: number = 0 /** The lines that contain decorations */ - decoratedLines: Set = new Set() + private decoratedLines: Set = new Set() /** The minimum margin to add so that the decoration is shown after all text. */ - margin: number = 0 - - /** Whether this worksheet has finished evaluating. */ - finished: boolean = false + private margin: number = 0 /** Remove all decorations and resets this worksheet. */ - reset() { + private reset(): void { this.decorationTypes.forEach(decoration => decoration.dispose()) this.insertedLines = 0 this.decoratedLines.clear() - this.margin = longestLine(this.document) + 5 - this.finished = false + this.margin = this.longestLine() + 5 } - /** All the worksheets */ - private static worksheets: Map = new Map() - /** - * If `document` is a worksheet, create a new worksheet for it, or return the existing one. */ - static getOrNewWorksheet(document: vscode.TextDocument): Worksheet | undefined { - if (!isWorksheet(document)) return - else { - const existing = Worksheet.worksheets.get(document) - if (existing) { - return existing - } else { - const newWorksheet = new Worksheet(document) - Worksheet.worksheets.set(document, newWorksheet) - return newWorksheet - } - } - } - - /** If it exists, remove the worksheet representing `document`. */ - static delete(document: vscode.TextDocument) { - Worksheet.worksheets.delete(document) - } -} - -/** - * The command key for evaluating a worksheet. Exposed to users as - * `Run worksheet`. - */ -export const worksheetEvaluateKey = "worksheet.evaluate" - -/** Remove the worksheet corresponding to the given document. */ -export function removeWorksheet(document: vscode.TextDocument) { - Worksheet.delete(document) -} - -/** Is this document a worksheet? */ -export function isWorksheet(document: vscode.TextDocument): boolean { - return document.fileName.endsWith(".sc") -} - -/** - * The VSCode command executed when the user select `Run worksheet`. - * - * We check whether the buffer is dirty, and if it is, we save it. Evaluation will then be - * triggered by file save. - * If the buffer is clean, we do the necessary preparation for worksheet (compute margin, - * remove blank lines, etc.) and check if the buffer has been changed by that. If it is, we save - * and the evaluation will be triggered by file save. - * If the buffer is still clean, call `evaluateWorksheet`. - */ -export function evaluateWorksheetCommand() { - const editor = vscode.window.activeTextEditor - if (editor) { - const document = editor.document - - if (document.isDirty) document.save() // This will trigger evaluation - else { - const worksheet = Worksheet.getOrNewWorksheet(document) - if (worksheet) { - _prepareWorksheet(worksheet).then(_ => { - if (document.isDirty) document.save() // This will trigger evaluation - else evaluateWorksheet(document) - }) - } - } + * Reset the "worksheet state" (margin and number of inserted lines), and + * removes redundant blank lines that have been inserted by a previous + * evaluation. + */ + prepareForRunning(): void { + this.removeRedundantBlankLines().then(_ => this.reset()) } -} - -/** - * Evaluate the worksheet in `document`, display a progress bar during evaluation. - */ -export function evaluateWorksheet(document: vscode.TextDocument): Thenable<{}> { - const worksheet = Worksheet.getOrNewWorksheet(document) - if (worksheet) { + /** + * Run the worksheet in `document`, display a progress bar during evaluation. + */ + run(): Thenable<{}> { return vscode.window.withProgress({ location: vscode.ProgressLocation.Notification, - title: "Evaluating worksheet", + title: "Run the worksheet", cancellable: true }, (_, token) => { - return client.sendRequest(WorksheetExecRequest.type, asWorksheetExecParams(document), token) + return this.client.sendRequest(WorksheetExecRequest.type, asWorksheetExecParams(this.document), token) }) - } else { - return Promise.reject() } -} -/** - * If the document that will be saved is a worksheet, resets the "worksheet state" - * (margin and number of inserted lines), and removes redundant blank lines that - * have been inserted by a previous evaluation. - * - * The file save operation is blocked until the worksheet is ready to be evaluated. - * - * @param event `TextDocumentWillSaveEvent`. - */ -export function prepareWorksheet(event: vscode.TextDocumentWillSaveEvent) { - const worksheet = Worksheet.getOrNewWorksheet(event.document) - if (worksheet) { - const setup = _prepareWorksheet(worksheet) - event.waitUntil(setup) + /** + * Parse and display the result of evaluating part of this worksheet. + * + * @param lineNumber The number of the line in the source that produced the result. + * @param evalResult The evaluation result. + * @param worksheet The worksheet that receives the result. + * @param editor The editor where to display the result. + * @return A `Thenable` that will insert necessary lines to fit the output + * and display the decorations upon completion. + */ + public displayResult(lineNumber: number, evalResult: string, editor: vscode.TextEditor) { + const resultLines = evalResult.trim().split(/\r\n|\r|\n/g) + + // The line where the next decoration should be put. + // It's the number of the line that produced the output, plus the number + // of lines that we've inserted so far. + let actualLine = lineNumber + this.insertedLines + + // If the output has more than one line, we need to insert blank lines + // below the line that produced the output to fit the output. + const addNewLinesEdit = new vscode.WorkspaceEdit() + if (resultLines.length > 1) { + const linesToInsert = resultLines.length - 1 + const editPos = new vscode.Position(actualLine + 1, 0) // add after the line + addNewLinesEdit.insert(editor.document.uri, editPos, "\n".repeat(linesToInsert)) + this.insertedLines += linesToInsert + } + + return vscode.workspace.applyEdit(addNewLinesEdit).then(_ => { + for (let line of resultLines) { + const decorationPosition = new vscode.Position(actualLine, 0) + const decorationMargin = this.margin - editor.document.lineAt(actualLine).text.length + const decorationType = this.createDecoration(decorationMargin, line) + this.decorationTypes.push(decorationType) + this.decoratedLines.add(actualLine) + + const decoration = { range: new vscode.Range(decorationPosition, decorationPosition), hoverMessage: line } + editor.setDecorations(decorationType, [decoration]) + actualLine += 1 + } + }) } -} -function _prepareWorksheet(worksheet: Worksheet) { - return removeRedundantBlankLines(worksheet).then(_ => worksheet.reset()) -} + /** + * Create a new `TextEditorDecorationType` showing `text`. The decoration + * will appear `margin` characters after the end of the line. + * + * @param margin The margin in characters between the end of the line + * and the decoration. + * @param text The text of the decoration. + * @return a new `TextEditorDecorationType`. + */ + private createDecoration(margin: number, text: string) { + return vscode.window.createTextEditorDecorationType({ + isWholeLine: true, + after: { + contentText: text, + margin: `0px 0px 0px ${margin}ch`, + fontStyle: "italic", + color: "light gray", + } + }) + } -/** - * Handle the result of evaluating part of a worksheet. - * This is called when we receive a `window/logMessage`. - * - * @param message The result of evaluating part of a worksheet. - */ -export function handleMessage(output: WorksheetPublishOutputParams) { + /** + * Finds the length in characters of the longest line of `document`. + * + * @param document The document to inspect. + * @return The length in characters of the longest line. + */ + private longestLine() { + let maxLength = 0 + const lineCount = this.document.lineCount + for (let i = 0; i < lineCount; ++i) { + let length = this.document.lineAt(i).text.length + maxLength = Math.max(maxLength, length) + } - const editor = vscode.window.visibleTextEditors.find(e => { - let uri = e.document.uri.toString() - return uri == output.textDocument.uri - }) + return maxLength + } - if (editor) { - const worksheet = Worksheet.getOrNewWorksheet(editor.document) + /** + * Remove the repeated blank lines in the source. + * + * Evaluating a worksheet can insert new lines in the worksheet so that the + * output of a line fits below the line. Before evaluation, we remove blank + * lines in the worksheet to keep its length under control. + * + * @param worksheet The worksheet where blank lines must be removed. + * @return A `Thenable` removing the blank lines upon completion. + */ + private removeRedundantBlankLines() { + + const document = this.document + const lineCount = document.lineCount + let rangesToRemove: vscode.Range[] = [] + let rangeStart = 0 + let rangeEnd = 0 + let inRange = true + + function addRange() { + inRange = false + if (rangeStart < rangeEnd) { + rangesToRemove.push(new vscode.Range(rangeStart, 0, rangeEnd, 0)) + } + return + } - if (worksheet) { - worksheetDisplayResult(output.line - 1, output.content, worksheet, editor) + for (let i = 0; i < lineCount; ++i) { + const isEmpty = document.lineAt(i).isEmptyOrWhitespace && this.hasDecoration(i) + if (inRange) { + if (isEmpty) rangeEnd += 1 + else addRange() + } else { + if (isEmpty) { + rangeStart = i + rangeEnd = i + 1 + inRange = true + } + } } - } -} -/** - * Create a new `TextEditorDecorationType` showing `text`. The decoration - * will appear `margin` characters after the end of the line. - * - * @param margin The margin in characters between the end of the line - * and the decoration. - * @param text The text of the decoration. - * @return a new `TextEditorDecorationType`. - */ -function worksheetCreateDecoration(margin: number, text: string) { - return vscode.window.createTextEditorDecorationType({ - isWholeLine: true, - after: { - contentText: text, - margin: `0px 0px 0px ${margin}ch`, - fontStyle: "italic", - color: "light gray", + if (inRange) { + rangeEnd = lineCount + addRange() } - }) -} -/** - * Finds the length in characters of the longest line of `document`. - * - * @param document The document to inspect. - * @return The length in characters of the longest line. - */ -function longestLine(document: vscode.TextDocument) { - let maxLength = 0 - const lineCount = document.lineCount - for (let i = 0; i < lineCount; ++i) { - let length = document.lineAt(i).text.length - maxLength = Math.max(maxLength, length) + return rangesToRemove.reverse().reduce((chain: Thenable, range) => { + return chain.then(_ => { + const edit = new vscode.WorkspaceEdit() + edit.delete(document.uri, range) + return vscode.workspace.applyEdit(edit) + }) + }, Promise.resolve(true)) } - return maxLength + private hasDecoration(line: number): boolean { + return this.decoratedLines.has(line) + } } -/** - * Remove the repeated blank lines in the source. - * - * Evaluating a worksheet can insert new lines in the worksheet so that the - * output of a line fits below the line. Before evaluation, we remove blank - * lines in the worksheet to keep its length under control. - * - * @param worksheet The worksheet where blank lines must be removed. - * @return A `Thenable` removing the blank lines upon completion. - */ -function removeRedundantBlankLines(worksheet: Worksheet) { - - function hasDecoration(line: number): boolean { - return worksheet.decoratedLines.has(line) +export class WorksheetProvider implements Disposable { + private disposables: Disposable[] = [] + private worksheets: Map = new Map() + + constructor( + readonly client: BaseLanguageClient, + readonly documentSelectors: vscode.DocumentSelector[]) { + this.disposables.push( + vscode.workspace.onWillSaveTextDocument(event => { + const worksheet = this.worksheetFor(event.document) + if (worksheet) { + // Block file saving until the worksheet is ready to be evaluated. + worksheet.prepareForRunning() + } + }), + vscode.workspace.onDidSaveTextDocument(document => { + const worksheet = this.worksheetFor(document) + if (worksheet) { + return worksheet.run() + } + }), + vscode.workspace.onDidCloseTextDocument(document => { + if (this.isWorksheet(document)) { + this.worksheets.delete(document) + } + }), + vscode.commands.registerCommand(worksheetEvaluateKey, () => { + this.evaluateWorksheetCommand() + }) + ) + client.onNotification(WorksheetPublishOutputNotification.type, params => { + this.handleMessage(params) + }) } - const document = worksheet.document - const lineCount = document.lineCount - let rangesToRemove: vscode.Range[] = [] - let rangeStart = 0 - let rangeEnd = 0 - let inRange = true - - function addRange() { - inRange = false - if (rangeStart < rangeEnd) { - rangesToRemove.push(new vscode.Range(rangeStart, 0, rangeEnd, 0)) - } - return + dispose() { + this.disposables.forEach(d => d.dispose()); + this.disposables = []; + } + + /** Is this document a worksheet? */ + private isWorksheet(document: vscode.TextDocument): boolean { + return this.documentSelectors.some(sel => vscode.languages.match(sel, document) > 0) } - for (let i = 0; i < lineCount; ++i) { - const isEmpty = document.lineAt(i).isEmptyOrWhitespace && hasDecoration(i) - if (inRange) { - if (isEmpty) rangeEnd += 1 - else addRange() - } else { - if (isEmpty) { - rangeStart = i - rangeEnd = i + 1 - inRange = true + /** If `document` is a worksheet, create a new worksheet for it, or return the existing one. */ + private worksheetFor(document: vscode.TextDocument): Worksheet | undefined { + if (!this.isWorksheet(document)) return + else { + const existing = this.worksheets.get(document) + if (existing) { + return existing + } else { + const newWorksheet = new Worksheet(document, this.client) + this.worksheets.set(document, newWorksheet) + return newWorksheet } } } - if (inRange) { - rangeEnd = lineCount - addRange() + /** + * The VSCode command executed when the user select `Run worksheet`. + * + * We check whether the buffer is dirty, and if it is, we save it. Evaluation will then be + * triggered by file save. + * If the buffer is clean, we do the necessary preparation for worksheet (compute margin, + * remove blank lines, etc.) and check if the buffer has been changed by that. If it is, we save + * and the evaluation will be triggered by file save. + * If the buffer is still clean, call `Worksheet#evaluate`. + */ + private evaluateWorksheetCommand() { + const editor = vscode.window.activeTextEditor + if (editor) { + const document = editor.document + const worksheet = this.worksheetFor(document) + if (worksheet) { + if (document.isDirty) document.save() // This will trigger evaluation + else { + worksheet.prepareForRunning() + if (document.isDirty) document.save() // This will trigger evaluation + else { + worksheet.run() + } + } + } + } } - return rangesToRemove.reverse().reduce((chain: Thenable, range) => { - return chain.then(_ => { - const edit = new vscode.WorkspaceEdit() - edit.delete(document.uri, range) - return vscode.workspace.applyEdit(edit) + /** + * Handle the result of evaluating part of a worksheet. + * This is called when we receive a `window/logMessage`. + * + * @param message The result of evaluating part of a worksheet. + */ + private handleMessage(output: WorksheetPublishOutputParams) { + const editor = vscode.window.visibleTextEditors.find(e => { + let uri = e.document.uri.toString() + return uri == output.textDocument.uri }) - }, Promise.resolve(true)) -} -/** - * Parse and display the result of evaluating part of a worksheet. - * - * @see worksheetCreateDecoration - * - * @param lineNumber The number of the line in the source that produced the result. - * @param evalResult The evaluation result. - * @param worksheet The worksheet that receives the result. - * @param editor The editor where to display the result. - * @return A `Thenable` that will insert necessary lines to fit the output - * and display the decorations upon completion. - */ -function worksheetDisplayResult(lineNumber: number, evalResult: string, worksheet: Worksheet, editor: vscode.TextEditor) { - - const resultLines = evalResult.trim().split(/\r\n|\r|\n/g) - const margin = worksheet.margin - - // The line where the next decoration should be put. - // It's the number of the line that produced the output, plus the number - // of lines that we've inserted so far. - let actualLine = lineNumber + worksheet.insertedLines - - // If the output has more than one line, we need to insert blank lines - // below the line that produced the output to fit the output. - const addNewLinesEdit = new vscode.WorkspaceEdit() - if (resultLines.length > 1) { - const linesToInsert = resultLines.length - 1 - const editPos = new vscode.Position(actualLine + 1, 0) // add after the line - addNewLinesEdit.insert(editor.document.uri, editPos, "\n".repeat(linesToInsert)) - worksheet.insertedLines += linesToInsert - } - - return vscode.workspace.applyEdit(addNewLinesEdit).then(_ => { - for (let line of resultLines) { - const decorationPosition = new vscode.Position(actualLine, 0) - const decorationMargin = margin - editor.document.lineAt(actualLine).text.length - const decorationType = worksheetCreateDecoration(decorationMargin, line) - worksheet.decorationTypes.push(decorationType) - worksheet.decoratedLines.add(actualLine) - - const decoration = { range: new vscode.Range(decorationPosition, decorationPosition), hoverMessage: line } - editor.setDecorations(decorationType, [decoration]) - actualLine += 1 + if (editor) { + const worksheet = this.worksheetFor(editor.document) + if (worksheet) { + worksheet.displayResult(output.line - 1, output.content, editor) + } } - }) + } } - From 59ceea99fe21b5326ed0035badc1f36e9454c5e7 Mon Sep 17 00:00:00 2001 From: Guillaume Martres Date: Wed, 10 Oct 2018 15:26:26 +0200 Subject: [PATCH 3/3] Standardize vocabulary around running the worksheet We used interchangeably the term "run", "evaluate" and "exec" for talking about running a worksheet. This commit does a lot of renaming to standardize on using "run" everywhere. --- .../languageserver/worksheet/Worksheet.scala | 8 ++-- .../worksheet/WorksheetClient.scala | 2 +- .../worksheet/WorksheetMessages.scala | 10 ++-- .../worksheet/WorksheetService.scala | 16 +++---- .../tools/languageserver/WorksheetTest.scala | 34 ++++++------- .../languageserver/util/CodeTester.scala | 22 ++++----- .../util/actions/WorksheetAction.scala | 10 ++-- .../util/actions/WorksheetCancel.scala | 2 +- ...sheetEvaluate.scala => WorksheetRun.scala} | 6 +-- .../util/server/TestClient.scala | 6 +-- vscode-dotty/package.json | 4 +- vscode-dotty/src/extension.ts | 2 +- vscode-dotty/src/features.ts | 10 ++-- vscode-dotty/src/protocol.ts | 16 +++---- vscode-dotty/src/worksheet.ts | 48 +++++++++---------- 15 files changed, 98 insertions(+), 98 deletions(-) rename language-server/test/dotty/tools/languageserver/util/actions/{WorksheetEvaluate.scala => WorksheetRun.scala} (76%) diff --git a/language-server/src/dotty/tools/languageserver/worksheet/Worksheet.scala b/language-server/src/dotty/tools/languageserver/worksheet/Worksheet.scala index 2f6e7aa7cd3f..4fcbbc86c398 100644 --- a/language-server/src/dotty/tools/languageserver/worksheet/Worksheet.scala +++ b/language-server/src/dotty/tools/languageserver/worksheet/Worksheet.scala @@ -15,15 +15,15 @@ import java.util.concurrent.CancellationException object Worksheet { /** - * Evaluate `tree` as a worksheet using the REPL. + * Run `tree` as a worksheet using the REPL. * * @param tree The top level object wrapping the worksheet. * @param sendMessage A mean of communicating the results of evaluation back. * @param cancelChecker A token to check whether execution should be cancelled. */ - def evaluate(tree: SourceTree, - sendMessage: (Int, String) => Unit, - cancelChecker: CancelChecker)( + def run(tree: SourceTree, + sendMessage: (Int, String) => Unit, + cancelChecker: CancelChecker)( implicit ctx: Context): Unit = synchronized { Evaluator.get(cancelChecker) match { diff --git a/language-server/src/dotty/tools/languageserver/worksheet/WorksheetClient.scala b/language-server/src/dotty/tools/languageserver/worksheet/WorksheetClient.scala index aa43d7df49ee..a9d75f34d899 100644 --- a/language-server/src/dotty/tools/languageserver/worksheet/WorksheetClient.scala +++ b/language-server/src/dotty/tools/languageserver/worksheet/WorksheetClient.scala @@ -12,6 +12,6 @@ trait WorksheetClient extends LanguageClient { * the specified output. */ @JsonNotification("worksheet/publishOutput") - def publishOutput(output: WorksheetExecOutput): Unit + def publishOutput(output: WorksheetRunOutput): Unit } diff --git a/language-server/src/dotty/tools/languageserver/worksheet/WorksheetMessages.scala b/language-server/src/dotty/tools/languageserver/worksheet/WorksheetMessages.scala index 05dcf27e8c31..a67b1add38a7 100644 --- a/language-server/src/dotty/tools/languageserver/worksheet/WorksheetMessages.scala +++ b/language-server/src/dotty/tools/languageserver/worksheet/WorksheetMessages.scala @@ -6,17 +6,17 @@ import org.eclipse.lsp4j.VersionedTextDocumentIdentifier // constructors to allow Gson to reflectively create instances on // deserialization without relying on sun.misc.Unsafe. -/** The parameter for the `worksheet/exec` request. */ -case class WorksheetExecParams(textDocument: VersionedTextDocumentIdentifier) { +/** The parameter for the `worksheet/run` request. */ +case class WorksheetRunParams(textDocument: VersionedTextDocumentIdentifier) { def this() = this(null) } -/** The response to a `worksheet/exec` request. */ -case class WorksheetExecResult(success: Boolean) { +/** The response to a `worksheet/run` request. */ +case class WorksheetRunResult(success: Boolean) { def this() = this(false) } /** The parameters to the `worksheet/publishOutput` notification. */ -case class WorksheetExecOutput(textDocument: VersionedTextDocumentIdentifier, line: Int, content: String) { +case class WorksheetRunOutput(textDocument: VersionedTextDocumentIdentifier, line: Int, content: String) { def this() = this(null, 0, null) } diff --git a/language-server/src/dotty/tools/languageserver/worksheet/WorksheetService.scala b/language-server/src/dotty/tools/languageserver/worksheet/WorksheetService.scala index c8cf1b277a64..3bc3876cc089 100644 --- a/language-server/src/dotty/tools/languageserver/worksheet/WorksheetService.scala +++ b/language-server/src/dotty/tools/languageserver/worksheet/WorksheetService.scala @@ -16,18 +16,18 @@ trait WorksheetService { thisServer: DottyLanguageServer => val worksheets: ConcurrentHashMap[URI, CompletableFuture[_]] = new ConcurrentHashMap() @JsonRequest - def exec(params: WorksheetExecParams): CompletableFuture[WorksheetExecResult] = thisServer.synchronized { + def run(params: WorksheetRunParams): CompletableFuture[WorksheetRunResult] = thisServer.synchronized { val uri = new URI(params.textDocument.getUri) val future = computeAsync { cancelChecker => try { val driver = driverFor(uri) - val sendMessage = (line: Int, msg: String) => client.publishOutput(WorksheetExecOutput(params.textDocument, line, msg)) - evaluateWorksheet(driver, uri, sendMessage, cancelChecker)(driver.currentCtx) - WorksheetExecResult(success = true) + val sendMessage = (line: Int, msg: String) => client.publishOutput(WorksheetRunOutput(params.textDocument, line, msg)) + runWorksheet(driver, uri, sendMessage, cancelChecker)(driver.currentCtx) + WorksheetRunResult(success = true) } catch { case _: Throwable => - WorksheetExecResult(success = false) + WorksheetRunResult(success = false) } finally { worksheets.remove(uri) } @@ -37,21 +37,21 @@ trait WorksheetService { thisServer: DottyLanguageServer => } /** - * Evaluate the worksheet at `uri`. + * Run the worksheet at `uri`. * * @param driver The driver for the project that contains the worksheet. * @param uri The URI of the worksheet. * @param sendMessage A mean of communicating the results of evaluation back. * @param cancelChecker Token to check whether evaluation was cancelled */ - private def evaluateWorksheet(driver: InteractiveDriver, + private def runWorksheet(driver: InteractiveDriver, uri: URI, sendMessage: (Int, String) => Unit, cancelChecker: CancelChecker)( implicit ctx: Context): Unit = { val trees = driver.openedTrees(uri) trees.headOption.foreach { tree => - Worksheet.evaluate(tree, sendMessage, cancelChecker) + Worksheet.run(tree, sendMessage, cancelChecker) } } } diff --git a/language-server/test/dotty/tools/languageserver/WorksheetTest.scala b/language-server/test/dotty/tools/languageserver/WorksheetTest.scala index 5b2dc2644e62..3893eaa68a0d 100644 --- a/language-server/test/dotty/tools/languageserver/WorksheetTest.scala +++ b/language-server/test/dotty/tools/languageserver/WorksheetTest.scala @@ -12,34 +12,34 @@ import java.lang.System.{lineSeparator => nl} class WorksheetTest { - @Test def evaluateExpression: Unit = { + @Test def runExpression: Unit = { ws"${m1}2 + 2".withSource - .evaluate(m1, "1:val res0: Int = 4") + .run(m1, "1:val res0: Int = 4") } - @Test def evaluateSimpleVal: Unit = { + @Test def runSimpleVal: Unit = { ws"${m1}val foo = 123".withSource - .evaluate(m1, "1:val foo: Int = 123") + .run(m1, "1:val foo: Int = 123") } @Test def usePreviousDefinition: Unit = { ws"""${m1}val foo = 123 val bar = foo + 1""".withSource - .evaluate(m1, "1:val foo: Int = 123", + .run(m1, "1:val foo: Int = 123", "2:val bar: Int = 124") } @Test def defineObject: Unit = { ws"""${m1}def foo(x: Int) = x + 1 foo(1)""".withSource - .evaluate(m1, "1:def foo(x: Int): Int", + .run(m1, "1:def foo(x: Int): Int", "2:val res0: Int = 2") } @Test def defineCaseClass: Unit = { ws"""${m1} case class Foo(x: Int) Foo(1)""".withSource - .evaluate(m1, "1:// defined case class Foo", + .run(m1, "1:// defined case class Foo", "2:val res0: Foo = Foo(1)") } @@ -48,7 +48,7 @@ class WorksheetTest { override def toString: String = "Foo" } new Foo(1)""".withSource - .evaluate(m1, "3:// defined class Foo", + .run(m1, "3:// defined class Foo", "4:val res0: Foo = Foo") } @@ -56,7 +56,7 @@ class WorksheetTest { ws"""${m1}new { override def toString: String = "Foo" }""".withSource - .evaluate(m1, "3:val res0: Object = Foo") + .run(m1, "3:val res0: Object = Foo") } @Test def defineAnonymousClass1: Unit = { @@ -65,14 +65,14 @@ class WorksheetTest { new Foo with Bar { override def toString: String = "Foo" }""".withSource - .evaluate(m1, "1:// defined class Foo", + .run(m1, "1:// defined class Foo", "2:// defined trait Bar", "5:val res0: Foo & Bar = Foo") } @Test def produceMultilineOutput: Unit = { ws"""${m1}1 to 3 foreach println""".withSource - .evaluate(m1, s"1:1${nl}2${nl}3") + .run(m1, s"1:1${nl}2${nl}3") } @Test def patternMatching0: Unit = { @@ -80,18 +80,18 @@ class WorksheetTest { case x if x % 2 == 0 => "even" case _ => "odd" }""".withSource - .evaluate(m1, "4:val res0: String = odd") + .run(m1, "4:val res0: String = odd") } @Test def patternMatching1: Unit = { ws"""${m1}val (foo, bar) = (1, 2)""".withSource - .evaluate(m1, s"1:val foo: Int = 1${nl}val bar: Int = 2") + .run(m1, s"1:val foo: Int = 1${nl}val bar: Int = 2") } @Test def evaluationException: Unit = { ws"""${m1}val foo = 1 / 0 val bar = 2""".withSource - .evaluateNonStrict(m1, "1:java.lang.ArithmeticException: / by zero", + .runNonStrict(m1, "1:java.lang.ArithmeticException: / by zero", "2:val bar: Int = 2") } @@ -200,19 +200,19 @@ class WorksheetTest { val bar = 2 while (true) {} val baz = 3""".withSource - .cancelEvaluation(m1, afterMs = 5000) + .cancelRun(m1, afterMs = 5000) } @Test def systemExit(): Unit = { ws"""${m1}println("Hello, world!") System.exit(0) println("Goodbye!")""".withSource - .evaluate(m1, "1:Hello, world!") + .run(m1, "1:Hello, world!") } @Test def outputOnStdErr(): Unit = { ws"""${m1}System.err.println("Oh no")""".withSource - .evaluate(m1, "1:Oh no") + .run(m1, "1:Oh no") } } diff --git a/language-server/test/dotty/tools/languageserver/util/CodeTester.scala b/language-server/test/dotty/tools/languageserver/util/CodeTester.scala index 42f5ed4fcc12..b9c91a0853d0 100644 --- a/language-server/test/dotty/tools/languageserver/util/CodeTester.scala +++ b/language-server/test/dotty/tools/languageserver/util/CodeTester.scala @@ -118,31 +118,31 @@ class CodeTester(sources: List[SourceWithPositions], actions: List[Action]) { doAction(new CodeSymbol(query, symbols)) /** - * Triggers evaluation of the worksheet specified by `marker`, verifies that the results of - * evaluation match `expected. + * Triggers running the worksheet specified by `marker`, verifies that the results of + * the run matches `expected`. * * @param marker A marker a identifies the worksheet to evaluate. * @param expected The expected output. * - * @see dotty.tools.languageserver.util.actions.WorksheetEvaluate + * @see dotty.tools.languageserver.util.actions.WorksheetRun */ - def evaluate(marker: CodeMarker, expected: String*): this.type = - doAction(new WorksheetEvaluate(marker, expected, strict = true)) + def run(marker: CodeMarker, expected: String*): this.type = + doAction(new WorksheetRun(marker, expected, strict = true)) /** - * Triggers evaluation of the worksheet specified by `marker`, verifies that each line of output + * Triggers running the worksheet specified by `marker`, verifies that each line of output * starts with `expected`. * * @param marker A marker a identifies the worksheet to evaluate. * @param expected The expected starts of output. * - * @see dotty.tools.languageserver.util.actions.WorksheetEvaluate + * @see dotty.tools.languageserver.util.actions.WorksheetRun */ - def evaluateNonStrict(marker: CodeMarker, expected: String*): this.type = - doAction(new WorksheetEvaluate(marker, expected, strict = false)) + def runNonStrict(marker: CodeMarker, expected: String*): this.type = + doAction(new WorksheetRun(marker, expected, strict = false)) /** - * Triggers evaluation of the worksheet specified by `marker`, then verifies that execution can be + * Triggers running the worksheet specified by `marker`, then verifies that execution can be * cancelled after `afterMs` milliseconds. * * @param marker A marker that identifier the worksheet to evaluate. @@ -150,7 +150,7 @@ class CodeTester(sources: List[SourceWithPositions], actions: List[Action]) { * * @see dotty.tools.languageserver.util.actions.WorksheetCancel */ - def cancelEvaluation(marker: CodeMarker, afterMs: Long): this.type = + def cancelRun(marker: CodeMarker, afterMs: Long): this.type = doAction(new WorksheetCancel(marker, afterMs)) private def doAction(action: Action): this.type = { diff --git a/language-server/test/dotty/tools/languageserver/util/actions/WorksheetAction.scala b/language-server/test/dotty/tools/languageserver/util/actions/WorksheetAction.scala index 78a890345fe4..aefbdf251e8c 100644 --- a/language-server/test/dotty/tools/languageserver/util/actions/WorksheetAction.scala +++ b/language-server/test/dotty/tools/languageserver/util/actions/WorksheetAction.scala @@ -1,6 +1,6 @@ package dotty.tools.languageserver.util.actions -import dotty.tools.languageserver.worksheet.{WorksheetExecOutput, WorksheetExecParams, WorksheetExecResult} +import dotty.tools.languageserver.worksheet.{WorksheetRunOutput, WorksheetRunParams, WorksheetRunResult} import dotty.tools.languageserver.util.embedded.CodeMarker import java.net.URI @@ -10,13 +10,13 @@ import org.eclipse.lsp4j.VersionedTextDocumentIdentifier abstract class WorksheetAction extends Action { - /** Triggers the evaluation of the worksheet. */ - def triggerEvaluation(marker: CodeMarker): Exec[CompletableFuture[WorksheetExecResult]] = { - server.exec(WorksheetExecParams(marker.toVersionedTextDocumentIdentifier)) + /** Triggers running the worksheet. */ + def triggerRun(marker: CodeMarker): Exec[CompletableFuture[WorksheetRunResult]] = { + server.run(WorksheetRunParams(marker.toVersionedTextDocumentIdentifier)) } /** The output of the worksheet that contains `marker`. */ - def worksheetOutput(marker: CodeMarker): Exec[List[WorksheetExecOutput]] = { + def worksheetOutput(marker: CodeMarker): Exec[List[WorksheetRunOutput]] = { val textDocument = marker.toVersionedTextDocumentIdentifier client.worksheetOutput.get.filter(_.textDocument.getUri == textDocument.getUri) } diff --git a/language-server/test/dotty/tools/languageserver/util/actions/WorksheetCancel.scala b/language-server/test/dotty/tools/languageserver/util/actions/WorksheetCancel.scala index f6374e72ac99..52ca3a288f8b 100644 --- a/language-server/test/dotty/tools/languageserver/util/actions/WorksheetCancel.scala +++ b/language-server/test/dotty/tools/languageserver/util/actions/WorksheetCancel.scala @@ -10,7 +10,7 @@ import java.util.concurrent.TimeUnit class WorksheetCancel(marker: CodeMarker, afterMs: Long) extends WorksheetAction { override def execute(): Exec[Unit] = { - val futureResult = triggerEvaluation(marker) + val futureResult = triggerRun(marker) Thread.sleep(afterMs) val cancelled = futureResult.cancel(true) diff --git a/language-server/test/dotty/tools/languageserver/util/actions/WorksheetEvaluate.scala b/language-server/test/dotty/tools/languageserver/util/actions/WorksheetRun.scala similarity index 76% rename from language-server/test/dotty/tools/languageserver/util/actions/WorksheetEvaluate.scala rename to language-server/test/dotty/tools/languageserver/util/actions/WorksheetRun.scala index 0c3af2403292..036e184618dc 100644 --- a/language-server/test/dotty/tools/languageserver/util/actions/WorksheetEvaluate.scala +++ b/language-server/test/dotty/tools/languageserver/util/actions/WorksheetRun.scala @@ -7,10 +7,10 @@ import java.util.concurrent.TimeUnit import org.junit.Assert.{assertEquals, assertTrue, fail} -class WorksheetEvaluate(marker: CodeMarker, expected: Seq[String], strict: Boolean) extends WorksheetAction { +class WorksheetRun(marker: CodeMarker, expected: Seq[String], strict: Boolean) extends WorksheetAction { override def execute(): Exec[Unit] = { - val result = triggerEvaluation(marker).get(30, TimeUnit.SECONDS) + val result = triggerRun(marker).get(30, TimeUnit.SECONDS) assertTrue(result.success) val logs = worksheetOutput(marker).map(out => s"${out.line}:${out.content}") @@ -26,5 +26,5 @@ class WorksheetEvaluate(marker: CodeMarker, expected: Seq[String], strict: Boole } override def show: PositionContext.PosCtx[String] = - s"WorksheetEvaluate(${marker.file}, ${expected})" + s"WorksheetRun(${marker.file}, ${expected})" } diff --git a/language-server/test/dotty/tools/languageserver/util/server/TestClient.scala b/language-server/test/dotty/tools/languageserver/util/server/TestClient.scala index b4aa5b815870..5d2effffa26e 100644 --- a/language-server/test/dotty/tools/languageserver/util/server/TestClient.scala +++ b/language-server/test/dotty/tools/languageserver/util/server/TestClient.scala @@ -1,6 +1,6 @@ package dotty.tools.languageserver.util.server -import dotty.tools.languageserver.worksheet.{WorksheetExecOutput, WorksheetClient} +import dotty.tools.languageserver.worksheet.{WorksheetRunOutput, WorksheetClient} import java.util.concurrent.CompletableFuture @@ -22,7 +22,7 @@ class TestClient extends WorksheetClient { val log = new Log[MessageParams] val diagnostics = new Log[PublishDiagnosticsParams] val telemetry = new Log[Any] - val worksheetOutput = new Log[WorksheetExecOutput] + val worksheetOutput = new Log[WorksheetRunOutput] override def logMessage(message: MessageParams) = { log += message @@ -45,7 +45,7 @@ class TestClient extends WorksheetClient { diagnostics += diagnosticsParams } - override def publishOutput(output: WorksheetExecOutput) = { + override def publishOutput(output: WorksheetRunOutput) = { worksheetOutput += output } diff --git a/vscode-dotty/package.json b/vscode-dotty/package.json index 1ae1c4f83af5..1697783f76d1 100644 --- a/vscode-dotty/package.json +++ b/vscode-dotty/package.json @@ -42,12 +42,12 @@ ], "commands": [ { - "command": "worksheet.evaluate", + "command": "dotty.worksheet.run", "title": "Run worksheet", "category": "Scala" }, { - "command": "worksheet.cancel", + "command": "dotty.worksheet.cancel", "title": "Cancel worksheet evaluation", "category": "Scala" } diff --git a/vscode-dotty/src/extension.ts b/vscode-dotty/src/extension.ts index 9eb6482c463f..6c72fa438389 100644 --- a/vscode-dotty/src/extension.ts +++ b/vscode-dotty/src/extension.ts @@ -192,7 +192,7 @@ function run(serverOptions: ServerOptions, isOldServer: boolean) { } client = new LanguageClient("dotty", "Dotty", serverOptions, clientOptions) - client.registerFeature(new features.WorksheetExecFeature(client)) + client.registerFeature(new features.WorksheetRunFeature(client)) if (isOldServer) enableOldServerWorkaround(client) diff --git a/vscode-dotty/src/features.ts b/vscode-dotty/src/features.ts index 247758bf595c..edccdc248ed0 100644 --- a/vscode-dotty/src/features.ts +++ b/vscode-dotty/src/features.ts @@ -7,7 +7,7 @@ import { generateUuid } from 'vscode-languageclient/lib/utils/uuid' import { DocumentSelector } from 'vscode-languageserver-protocol' import { Disposable } from 'vscode-jsonrpc' -import { WorksheetExecRequest } from './protocol' +import { WorksheetRunRequest } from './protocol' import { WorksheetProvider } from './worksheet' // Remove this if @@ -21,7 +21,7 @@ function ensure(target: T, key: K): T[K] { export interface WorksheetClientCapabilities { worksheet?: { - exec?: { + run?: { dynamicRegistration?: boolean } } @@ -34,13 +34,13 @@ export interface WorksheetServerCapabilities { worksheetRunProvider?: boolean } -export class WorksheetExecFeature extends TextDocumentFeature { +export class WorksheetRunFeature extends TextDocumentFeature { constructor(client: BaseLanguageClient) { - super(client, WorksheetExecRequest.type) + super(client, WorksheetRunRequest.type) } public fillClientCapabilities(capabilities: ClientCapabilities & WorksheetClientCapabilities): void { - ensure(ensure(capabilities, "worksheet")!, "exec")!.dynamicRegistration = true + ensure(ensure(capabilities, "worksheet")!, "run")!.dynamicRegistration = true } public initialize(capabilities: ServerCapabilities & WorksheetServerCapabilities, documentSelector: DocumentSelector): void { diff --git a/vscode-dotty/src/protocol.ts b/vscode-dotty/src/protocol.ts index 4719c6b283d1..764385fec5b2 100644 --- a/vscode-dotty/src/protocol.ts +++ b/vscode-dotty/src/protocol.ts @@ -4,13 +4,13 @@ import { VersionedTextDocumentIdentifier } from 'vscode-languageserver-protocol' import { client } from './extension' -/** The parameters for the `worksheet/exec` request. */ -export interface WorksheetExecParams { +/** The parameters for the `worksheet/run` request. */ +export interface WorksheetRunParams { textDocument: VersionedTextDocumentIdentifier } -/** The result of the `worksheet/exec` request. */ -export interface WorksheetExecResult { +/** The result of the `worksheet/run` request. */ +export interface WorksheetRunResult { success: boolean } @@ -30,15 +30,15 @@ export function asVersionedTextDocumentIdentifier(textDocument: vscode.TextDocum } } -export function asWorksheetExecParams(textDocument: vscode.TextDocument): WorksheetExecParams { +export function asWorksheetRunParams(textDocument: vscode.TextDocument): WorksheetRunParams { return { textDocument: asVersionedTextDocumentIdentifier(textDocument) } } -/** The `worksheet/exec` request */ -export namespace WorksheetExecRequest { - export const type = new RequestType("worksheet/exec") +/** The `worksheet/run` request */ +export namespace WorksheetRunRequest { + export const type = new RequestType("worksheet/run") } /** The `worksheet/publishOutput` notification */ diff --git a/vscode-dotty/src/worksheet.ts b/vscode-dotty/src/worksheet.ts index 8db6b90c8941..780efb5fec99 100644 --- a/vscode-dotty/src/worksheet.ts +++ b/vscode-dotty/src/worksheet.ts @@ -1,17 +1,17 @@ import * as vscode from 'vscode' import { - asWorksheetExecParams, WorksheetExecRequest, WorksheetExecParams, + asWorksheetRunParams, WorksheetRunRequest, WorksheetRunParams, WorksheetPublishOutputParams, WorksheetPublishOutputNotification } from './protocol' import { BaseLanguageClient, DocumentSelector } from 'vscode-languageclient' import { Disposable } from 'vscode-jsonrpc' /** - * The command key for evaluating a worksheet. Exposed to users as + * The command key for running a worksheet. Exposed to users as * `Run worksheet`. */ -export const worksheetEvaluateKey = "worksheet.evaluate" +export const worksheetRunKey = "dotty.worksheet.run" /** A worksheet managed by vscode */ class Worksheet { @@ -42,14 +42,14 @@ class Worksheet { /** * Reset the "worksheet state" (margin and number of inserted lines), and * removes redundant blank lines that have been inserted by a previous - * evaluation. + * run. */ prepareForRunning(): void { this.removeRedundantBlankLines().then(_ => this.reset()) } /** - * Run the worksheet in `document`, display a progress bar during evaluation. + * Run the worksheet in `document`, display a progress bar during the run. */ run(): Thenable<{}> { return vscode.window.withProgress({ @@ -57,22 +57,22 @@ class Worksheet { title: "Run the worksheet", cancellable: true }, (_, token) => { - return this.client.sendRequest(WorksheetExecRequest.type, asWorksheetExecParams(this.document), token) + return this.client.sendRequest(WorksheetRunRequest.type, asWorksheetRunParams(this.document), token) }) } /** - * Parse and display the result of evaluating part of this worksheet. + * Parse and display the result of running part of this worksheet. * * @param lineNumber The number of the line in the source that produced the result. - * @param evalResult The evaluation result. + * @param runResult The result itself. * @param worksheet The worksheet that receives the result. * @param editor The editor where to display the result. * @return A `Thenable` that will insert necessary lines to fit the output * and display the decorations upon completion. */ - public displayResult(lineNumber: number, evalResult: string, editor: vscode.TextEditor) { - const resultLines = evalResult.trim().split(/\r\n|\r|\n/g) + public displayResult(lineNumber: number, runResult: string, editor: vscode.TextEditor) { + const resultLines = runResult.trim().split(/\r\n|\r|\n/g) // The line where the next decoration should be put. // It's the number of the line that produced the output, plus the number @@ -145,8 +145,8 @@ class Worksheet { /** * Remove the repeated blank lines in the source. * - * Evaluating a worksheet can insert new lines in the worksheet so that the - * output of a line fits below the line. Before evaluation, we remove blank + * Running a worksheet can insert new lines in the worksheet so that the + * output of a line fits below the line. Before a run, we remove blank * lines in the worksheet to keep its length under control. * * @param worksheet The worksheet where blank lines must be removed. @@ -213,7 +213,7 @@ export class WorksheetProvider implements Disposable { vscode.workspace.onWillSaveTextDocument(event => { const worksheet = this.worksheetFor(event.document) if (worksheet) { - // Block file saving until the worksheet is ready to be evaluated. + // Block file saving until the worksheet is ready to be run. worksheet.prepareForRunning() } }), @@ -228,8 +228,8 @@ export class WorksheetProvider implements Disposable { this.worksheets.delete(document) } }), - vscode.commands.registerCommand(worksheetEvaluateKey, () => { - this.evaluateWorksheetCommand() + vscode.commands.registerCommand(worksheetRunKey, () => { + this.runWorksheetCommand() }) ) client.onNotification(WorksheetPublishOutputNotification.type, params => { @@ -265,23 +265,23 @@ export class WorksheetProvider implements Disposable { /** * The VSCode command executed when the user select `Run worksheet`. * - * We check whether the buffer is dirty, and if it is, we save it. Evaluation will then be + * We check whether the buffer is dirty, and if it is, we save it. Running the worksheet will then be * triggered by file save. * If the buffer is clean, we do the necessary preparation for worksheet (compute margin, * remove blank lines, etc.) and check if the buffer has been changed by that. If it is, we save - * and the evaluation will be triggered by file save. - * If the buffer is still clean, call `Worksheet#evaluate`. + * and the run will be triggered by file save. + * If the buffer is still clean, call `Worksheet#run`. */ - private evaluateWorksheetCommand() { + private runWorksheetCommand() { const editor = vscode.window.activeTextEditor if (editor) { const document = editor.document const worksheet = this.worksheetFor(document) if (worksheet) { - if (document.isDirty) document.save() // This will trigger evaluation + if (document.isDirty) document.save() // This will trigger running the worksheet else { worksheet.prepareForRunning() - if (document.isDirty) document.save() // This will trigger evaluation + if (document.isDirty) document.save() // This will trigger running the worksheet else { worksheet.run() } @@ -291,10 +291,10 @@ export class WorksheetProvider implements Disposable { } /** - * Handle the result of evaluating part of a worksheet. - * This is called when we receive a `window/logMessage`. + * Handle the result of running part of a worksheet. + * This is called when we receive a `worksheet/publishOutput`. * - * @param message The result of evaluating part of a worksheet. + * @param message The result of running part of a worksheet. */ private handleMessage(output: WorksheetPublishOutputParams) { const editor = vscode.window.visibleTextEditors.find(e => {