diff --git a/editors/code/src/commands/apply_source_change.ts b/editors/code/src/commands/apply_source_change.ts new file mode 100644 index 0000000000..67765e5a3c --- /dev/null +++ b/editors/code/src/commands/apply_source_change.ts @@ -0,0 +1,58 @@ +import * as vscode from 'vscode'; +import * as lc from 'vscode-languageclient'; + +import { Server } from '../server'; + +interface FileSystemEdit { + type: string; + uri?: string; + src?: string; + dst?: string; +} + +export interface SourceChange { + label: string; + sourceFileEdits: lc.TextDocumentEdit[]; + fileSystemEdits: FileSystemEdit[]; + cursorPosition?: lc.TextDocumentPositionParams; +} + +export async function handle(change: SourceChange) { + const wsEdit = new vscode.WorkspaceEdit(); + for (const sourceEdit of change.sourceFileEdits) { + const uri = Server.client.protocol2CodeConverter.asUri(sourceEdit.textDocument.uri); + const edits = Server.client.protocol2CodeConverter.asTextEdits(sourceEdit.edits); + wsEdit.set(uri, edits); + } + let created; + let moved; + for (const fsEdit of change.fileSystemEdits) { + switch (fsEdit.type) { + case 'createFile': + const uri = vscode.Uri.parse(fsEdit.uri!); + wsEdit.createFile(uri); + created = uri; + break; + case 'moveFile': + const src = vscode.Uri.parse(fsEdit.src!); + const dst = vscode.Uri.parse(fsEdit.dst!); + wsEdit.renameFile(src, dst); + moved = dst; + break; + } + } + const toOpen = created || moved; + const toReveal = change.cursorPosition; + await vscode.workspace.applyEdit(wsEdit); + if (toOpen) { + const doc = await vscode.workspace.openTextDocument(toOpen); + await vscode.window.showTextDocument(doc); + } else if (toReveal) { + const uri = Server.client.protocol2CodeConverter.asUri(toReveal.textDocument.uri); + const position = Server.client.protocol2CodeConverter.asPosition(toReveal.position); + const editor = vscode.window.activeTextEditor; + if (!editor || editor.document.uri.toString() !== uri.toString()) { return; } + if (!editor.selection.isEmpty) { return; } + editor!.selection = new vscode.Selection(position, position); + } +} diff --git a/editors/code/src/commands/extend_selection.ts b/editors/code/src/commands/extend_selection.ts new file mode 100644 index 0000000000..cdc3d10fb3 --- /dev/null +++ b/editors/code/src/commands/extend_selection.ts @@ -0,0 +1,29 @@ +import * as vscode from 'vscode'; + +import { Range, TextDocumentIdentifier } from 'vscode-languageclient'; +import { Server } from '../server'; + +interface ExtendSelectionParams { + textDocument: TextDocumentIdentifier; + selections: Range[]; +} + +interface ExtendSelectionResult { + selections: Range[]; +} + +export async function handle() { + const editor = vscode.window.activeTextEditor; + if (editor == null || editor.document.languageId !== 'rust') { return; } + const request: ExtendSelectionParams = { + selections: editor.selections.map((s) => { + return Server.client.code2ProtocolConverter.asRange(s); + }), + textDocument: { uri: editor.document.uri.toString() }, + }; + const response = await Server.client.sendRequest('m/extendSelection', request); + editor.selections = response.selections.map((range: Range) => { + const r = Server.client.protocol2CodeConverter.asRange(range); + return new vscode.Selection(r.start, r.end); + }); +} diff --git a/editors/code/src/commands/index.ts b/editors/code/src/commands/index.ts new file mode 100644 index 0000000000..dfdcd64545 --- /dev/null +++ b/editors/code/src/commands/index.ts @@ -0,0 +1,17 @@ +import * as applySourceChange from './apply_source_change'; +import * as extendSelection from './extend_selection'; +import * as joinLines from './join_lines'; +import * as matchingBrace from './matching_brace'; +import * as parentModule from './parent_module'; +import * as runnables from './runnables'; +import * as syntaxTree from './syntaxTree'; + +export { + applySourceChange, + extendSelection, + joinLines, + matchingBrace, + parentModule, + runnables, + syntaxTree, +}; diff --git a/editors/code/src/commands/join_lines.ts b/editors/code/src/commands/join_lines.ts new file mode 100644 index 0000000000..526b698ccd --- /dev/null +++ b/editors/code/src/commands/join_lines.ts @@ -0,0 +1,21 @@ +import * as vscode from 'vscode'; + +import { Range, TextDocumentIdentifier } from 'vscode-languageclient'; +import { Server } from '../server'; +import { handle as applySourceChange, SourceChange } from './apply_source_change'; + +interface JoinLinesParams { + textDocument: TextDocumentIdentifier; + range: Range; +} + +export async function handle() { + const editor = vscode.window.activeTextEditor; + if (editor == null || editor.document.languageId !== 'rust') { return; } + const request: JoinLinesParams = { + range: Server.client.code2ProtocolConverter.asRange(editor.selection), + textDocument: { uri: editor.document.uri.toString() }, + }; + const change = await Server.client.sendRequest('m/joinLines', request); + await applySourceChange(change); +} diff --git a/editors/code/src/commands/matching_brace.ts b/editors/code/src/commands/matching_brace.ts new file mode 100644 index 0000000000..a80446a8f5 --- /dev/null +++ b/editors/code/src/commands/matching_brace.ts @@ -0,0 +1,27 @@ +import * as vscode from 'vscode'; + +import { Position, TextDocumentIdentifier } from 'vscode-languageclient'; +import { Server } from '../server'; + +interface FindMatchingBraceParams { + textDocument: TextDocumentIdentifier; + offsets: Position[]; +} + +export async function handle() { + const editor = vscode.window.activeTextEditor; + if (editor == null || editor.document.languageId !== 'rust') { return; } + const request: FindMatchingBraceParams = { + textDocument: { uri: editor.document.uri.toString() }, + offsets: editor.selections.map((s) => { + return Server.client.code2ProtocolConverter.asPosition(s.active); + }), + }; + const response = await Server.client.sendRequest('m/findMatchingBrace', request); + editor.selections = editor.selections.map((sel, idx) => { + const active = Server.client.protocol2CodeConverter.asPosition(response[idx]); + const anchor = sel.isEmpty ? active : sel.anchor; + return new vscode.Selection(anchor, active); + }); + editor.revealRange(editor.selection); +} diff --git a/editors/code/src/commands/parent_module.ts b/editors/code/src/commands/parent_module.ts new file mode 100644 index 0000000000..d66fb30264 --- /dev/null +++ b/editors/code/src/commands/parent_module.ts @@ -0,0 +1,22 @@ +import * as vscode from 'vscode'; + +import { Location, TextDocumentIdentifier } from 'vscode-languageclient'; +import { Server } from '../server'; + +export async function handle() { + const editor = vscode.window.activeTextEditor; + if (editor == null || editor.document.languageId !== 'rust') { return; } + const request: TextDocumentIdentifier = { + uri: editor.document.uri.toString(), + }; + const response = await Server.client.sendRequest('m/parentModule', request); + const loc = response[0]; + if (loc == null) { return; } + const uri = Server.client.protocol2CodeConverter.asUri(loc.uri); + const range = Server.client.protocol2CodeConverter.asRange(loc.range); + + const doc = await vscode.workspace.openTextDocument(uri); + const e = await vscode.window.showTextDocument(doc); + e.selection = new vscode.Selection(range.start, range.start); + e.revealRange(range, vscode.TextEditorRevealType.InCenter); +} diff --git a/editors/code/src/commands/runnables.ts b/editors/code/src/commands/runnables.ts new file mode 100644 index 0000000000..40f590dceb --- /dev/null +++ b/editors/code/src/commands/runnables.ts @@ -0,0 +1,88 @@ +import * as vscode from 'vscode'; +import * as lc from 'vscode-languageclient'; +import { Server } from '../server'; + +interface RunnablesParams { + textDocument: lc.TextDocumentIdentifier; + position?: lc.Position; +} + +interface Runnable { + range: lc.Range; + label: string; + bin: string; + args: string[]; + env: { [index: string]: string }; +} + +class RunnableQuickPick implements vscode.QuickPickItem { + public label: string; + public description?: string | undefined; + public detail?: string | undefined; + public picked?: boolean | undefined; + + constructor(public runnable: Runnable) { + this.label = runnable.label; + } +} + +interface CargoTaskDefinition extends vscode.TaskDefinition { + type: 'cargo'; + label: string; + command: string; + args: string[]; + env?: { [key: string]: string }; +} + +function createTask(spec: Runnable): vscode.Task { + const TASK_SOURCE = 'Rust'; + const definition: CargoTaskDefinition = { + type: 'cargo', + label: 'cargo', + command: spec.bin, + args: spec.args, + env: spec.env, + }; + + const execCmd = `${definition.command} ${definition.args.join(' ')}`; + const execOption: vscode.ShellExecutionOptions = { + cwd: '.', + env: definition.env, + }; + const exec = new vscode.ShellExecution(`clear; ${execCmd}`, execOption); + + const f = vscode.workspace.workspaceFolders![0]; + const t = new vscode.Task(definition, f, definition.label, TASK_SOURCE, exec, ['$rustc']); + return t; +} + +let prevRunnable: RunnableQuickPick | undefined; +export async function handle() { + const editor = vscode.window.activeTextEditor; + if (editor == null || editor.document.languageId !== 'rust') { return; } + const textDocument: lc.TextDocumentIdentifier = { + uri: editor.document.uri.toString(), + }; + const params: RunnablesParams = { + textDocument, + position: Server.client.code2ProtocolConverter.asPosition(editor.selection.active), + }; + const runnables = await Server.client.sendRequest('m/runnables', params); + const items: RunnableQuickPick[] = []; + if (prevRunnable) { + items.push(prevRunnable); + } + for (const r of runnables) { + if (prevRunnable && JSON.stringify(prevRunnable.runnable) === JSON.stringify(r)) { + continue; + } + items.push(new RunnableQuickPick(r)); + } + const item = await vscode.window.showQuickPick(items); + if (item) { + item.detail = 'rerun'; + prevRunnable = item; + const task = createTask(item.runnable); + return await vscode.tasks.executeTask(task); + } +} diff --git a/editors/code/src/commands/syntaxTree.ts b/editors/code/src/commands/syntaxTree.ts new file mode 100644 index 0000000000..dcb721eee5 --- /dev/null +++ b/editors/code/src/commands/syntaxTree.ts @@ -0,0 +1,38 @@ +import * as vscode from 'vscode'; +import { TextDocumentIdentifier } from 'vscode-languageclient'; + +import { Server } from '../server'; + +export const syntaxTreeUri = vscode.Uri.parse('ra-lsp://syntaxtree'); + +export class TextDocumentContentProvider implements vscode.TextDocumentContentProvider { + public eventEmitter = new vscode.EventEmitter(); + public syntaxTree: string = 'Not available'; + + public provideTextDocumentContent(uri: vscode.Uri): vscode.ProviderResult { + const editor = vscode.window.activeTextEditor; + if (editor == null) { return ''; } + const request: SyntaxTreeParams = { + textDocument: { uri: editor.document.uri.toString() }, + }; + return Server.client.sendRequest('m/syntaxTree', request); + } + + get onDidChange(): vscode.Event { + return this.eventEmitter.event; + } +} + +interface SyntaxTreeParams { + textDocument: TextDocumentIdentifier; +} + +type SyntaxTreeResult = string; + +// Opens the virtual file that will show the syntax tree +// +// The contents of the file come from the `TextDocumentContentProvider` +export async function handle() { + const document = await vscode.workspace.openTextDocument(syntaxTreeUri); + return vscode.window.showTextDocument(document, vscode.ViewColumn.Two, true); +} diff --git a/editors/code/src/config.ts b/editors/code/src/config.ts new file mode 100644 index 0000000000..740b5be206 --- /dev/null +++ b/editors/code/src/config.ts @@ -0,0 +1,23 @@ +import * as vscode from 'vscode'; + +import { Server } from './server'; + +export class Config { + public highlightingOn = true; + + constructor() { + vscode.workspace.onDidChangeConfiguration((_) => this.userConfigChanged()); + this.userConfigChanged(); + } + + public userConfigChanged() { + const config = vscode.workspace.getConfiguration('ra-lsp'); + if (config.has('highlightingOn')) { + this.highlightingOn = config.get('highlightingOn') as boolean; + } + + if (!this.highlightingOn && Server) { + Server.highlighter.removeHighlights(); + } + } +} diff --git a/editors/code/src/events/change_active_text_editor.ts b/editors/code/src/events/change_active_text_editor.ts new file mode 100644 index 0000000000..3440aa0c37 --- /dev/null +++ b/editors/code/src/events/change_active_text_editor.ts @@ -0,0 +1,14 @@ +import { TextEditor } from 'vscode'; +import { TextDocumentIdentifier } from 'vscode-languageclient'; + +import { Decoration } from '../highlighting'; +import { Server } from '../server'; + +export async function handle(editor: TextEditor | undefined) { + if (!Server.config.highlightingOn || !editor || editor.document.languageId !== 'rust') { return; } + const params: TextDocumentIdentifier = { + uri: editor.document.uri.toString(), + }; + const decorations = await Server.client.sendRequest('m/decorationsRequest', params); + Server.highlighter.setHighlights(editor, decorations); +} diff --git a/editors/code/src/events/change_text_document.ts b/editors/code/src/events/change_text_document.ts new file mode 100644 index 0000000000..b3000e0261 --- /dev/null +++ b/editors/code/src/events/change_text_document.ts @@ -0,0 +1,19 @@ +import * as vscode from 'vscode'; + +import { syntaxTreeUri, TextDocumentContentProvider } from '../commands/syntaxTree'; + +export function createHandler(textDocumentContentProvider: TextDocumentContentProvider) { + return (event: vscode.TextDocumentChangeEvent) => { + const doc = event.document; + if (doc.languageId !== 'rust') { return; } + afterLs(() => { + textDocumentContentProvider.eventEmitter.fire(syntaxTreeUri); + }); + }; +} + +// We need to order this after LS updates, but there's no API for that. +// Hence, good old setTimeout. +function afterLs(f: () => any) { + setTimeout(f, 10); +} diff --git a/editors/code/src/events/index.ts b/editors/code/src/events/index.ts new file mode 100644 index 0000000000..b570a7a926 --- /dev/null +++ b/editors/code/src/events/index.ts @@ -0,0 +1,7 @@ +import * as changeActiveTextEditor from './change_active_text_editor'; +import * as changeTextDocument from './change_text_document'; + +export { + changeActiveTextEditor, + changeTextDocument, +}; diff --git a/editors/code/src/extension.ts b/editors/code/src/extension.ts index fde6a480d8..44e74f4cc9 100644 --- a/editors/code/src/extension.ts +++ b/editors/code/src/extension.ts @@ -1,434 +1,56 @@ -'use strict'; import * as vscode from 'vscode'; -import * as lc from 'vscode-languageclient' +import * as lc from 'vscode-languageclient'; -let client: lc.LanguageClient; - -let uris = { - syntaxTree: vscode.Uri.parse('ra-lsp://syntaxtree') -} - -let highlightingOn = true; +import * as commands from './commands'; +import { TextDocumentContentProvider } from './commands/syntaxTree'; +import * as events from './events'; +import * as notifications from './notifications'; +import { Server } from './server'; export function activate(context: vscode.ExtensionContext) { - let applyHighlightingOn = () => { - let config = vscode.workspace.getConfiguration('ra-lsp'); - if (config.has('highlightingOn')) { - highlightingOn = config.get('highlightingOn') as boolean; - }; - - if (!highlightingOn) { - removeHighlights(); - } - }; - - // Apply the highlightingOn config now and whenever the config changes - applyHighlightingOn(); - vscode.workspace.onDidChangeConfiguration(_ => { - applyHighlightingOn(); - }); - - let textDocumentContentProvider = new TextDocumentContentProvider() - let dispose = (disposable: vscode.Disposable) => { + function disposeOnDeactivation(disposable: vscode.Disposable) { context.subscriptions.push(disposable); } - let registerCommand = (name: string, f: any) => { - dispose(vscode.commands.registerCommand(name, f)) + + function registerCommand(name: string, f: any) { + disposeOnDeactivation(vscode.commands.registerCommand(name, f)); } - registerCommand('ra-lsp.syntaxTree', () => openDoc(uris.syntaxTree)) - registerCommand('ra-lsp.extendSelection', async () => { - let editor = vscode.window.activeTextEditor - if (editor == null || editor.document.languageId != "rust") return - let request: ExtendSelectionParams = { - textDocument: { uri: editor.document.uri.toString() }, - selections: editor.selections.map((s) => { - return client.code2ProtocolConverter.asRange(s) - }) - } - let response = await client.sendRequest("m/extendSelection", request) - editor.selections = response.selections.map((range) => { - let r = client.protocol2CodeConverter.asRange(range) - return new vscode.Selection(r.start, r.end) - }) - }) - registerCommand('ra-lsp.matchingBrace', async () => { - let editor = vscode.window.activeTextEditor - if (editor == null || editor.document.languageId != "rust") return - let request: FindMatchingBraceParams = { - textDocument: { uri: editor.document.uri.toString() }, - offsets: editor.selections.map((s) => { - return client.code2ProtocolConverter.asPosition(s.active) - }) - } - let response = await client.sendRequest("m/findMatchingBrace", request) - editor.selections = editor.selections.map((sel, idx) => { - let active = client.protocol2CodeConverter.asPosition(response[idx]) - let anchor = sel.isEmpty ? active : sel.anchor - return new vscode.Selection(anchor, active) - }) - editor.revealRange(editor.selection) - }) - registerCommand('ra-lsp.joinLines', async () => { - let editor = vscode.window.activeTextEditor - if (editor == null || editor.document.languageId != "rust") return - let request: JoinLinesParams = { - textDocument: { uri: editor.document.uri.toString() }, - range: client.code2ProtocolConverter.asRange(editor.selection), - } - let change = await client.sendRequest("m/joinLines", request) - await applySourceChange(change) - }) - registerCommand('ra-lsp.parentModule', async () => { - let editor = vscode.window.activeTextEditor - if (editor == null || editor.document.languageId != "rust") return - let request: lc.TextDocumentIdentifier = { - uri: editor.document.uri.toString() - } - let response = await client.sendRequest("m/parentModule", request) - let loc = response[0] - if (loc == null) return - let uri = client.protocol2CodeConverter.asUri(loc.uri) - let range = client.protocol2CodeConverter.asRange(loc.range) + // Commands are requests from vscode to the language server + registerCommand('ra-lsp.syntaxTree', commands.syntaxTree.handle); + registerCommand('ra-lsp.extendSelection', commands.extendSelection.handle); + registerCommand('ra-lsp.matchingBrace', commands.matchingBrace.handle); + registerCommand('ra-lsp.joinLines', commands.joinLines.handle); + registerCommand('ra-lsp.parentModule', commands.parentModule.handle); + registerCommand('ra-lsp.run', commands.runnables.handle); + registerCommand('ra-lsp.applySourceChange', commands.applySourceChange.handle); - let doc = await vscode.workspace.openTextDocument(uri) - let e = await vscode.window.showTextDocument(doc) - e.selection = new vscode.Selection(range.start, range.start) - e.revealRange(range, vscode.TextEditorRevealType.InCenter) - }) + // Notifications are events triggered by the language server + const allNotifications: Iterable<[string, lc.GenericNotificationHandler]> = [ + ['m/publishDecorations', notifications.publishDecorations.handle], + ]; - let prevRunnable: RunnableQuickPick | undefined = undefined - registerCommand('ra-lsp.run', async () => { - let editor = vscode.window.activeTextEditor - if (editor == null || editor.document.languageId != "rust") return - let textDocument: lc.TextDocumentIdentifier = { - uri: editor.document.uri.toString() - } - let params: RunnablesParams = { - textDocument, - position: client.code2ProtocolConverter.asPosition(editor.selection.active) - } - let runnables = await client.sendRequest('m/runnables', params) - let items: RunnableQuickPick[] = [] - if (prevRunnable) { - items.push(prevRunnable) - } - for (let r of runnables) { - if (prevRunnable && JSON.stringify(prevRunnable.runnable) == JSON.stringify(r)) { - continue - } - items.push(new RunnableQuickPick(r)) - } - let item = await vscode.window.showQuickPick(items) - if (item) { - item.detail = "rerun" - prevRunnable = item - let task = createTask(item.runnable) - return await vscode.tasks.executeTask(task) - } - }) - registerCommand('ra-lsp.applySourceChange', applySourceChange) + // The events below are plain old javascript events, triggered and handled by vscode + vscode.window.onDidChangeActiveTextEditor(events.changeActiveTextEditor.handle); - dispose(vscode.workspace.registerTextDocumentContentProvider( + const textDocumentContentProvider = new TextDocumentContentProvider(); + disposeOnDeactivation(vscode.workspace.registerTextDocumentContentProvider( 'ra-lsp', - textDocumentContentProvider - )) - startServer() - vscode.workspace.onDidChangeTextDocument((event: vscode.TextDocumentChangeEvent) => { - let doc = event.document - if (doc.languageId != "rust") return - afterLs(() => { - textDocumentContentProvider.eventEmitter.fire(uris.syntaxTree) - }) - }, null, context.subscriptions) - vscode.window.onDidChangeActiveTextEditor(async (editor) => { - if (!highlightingOn || !editor || editor.document.languageId != 'rust') return - let params: lc.TextDocumentIdentifier = { - uri: editor.document.uri.toString() - } - let decorations = await client.sendRequest("m/decorationsRequest", params) - setHighlights(editor, decorations) - }) -} + textDocumentContentProvider, + )); -// We need to order this after LS updates, but there's no API for that. -// Hence, good old setTimeout. -function afterLs(f: () => any) { - setTimeout(f, 10) + vscode.workspace.onDidChangeTextDocument( + events.changeTextDocument.createHandler(textDocumentContentProvider), + null, + context.subscriptions); + + // Start the language server, finally! + Server.start(allNotifications); } export function deactivate(): Thenable { - if (!client) { + if (!Server.client) { return Promise.resolve(); } - return client.stop(); -} - -function startServer() { - let run: lc.Executable = { - command: "ra_lsp_server", - options: { cwd: "." } - } - let serverOptions: lc.ServerOptions = { - run, - debug: run - }; - - let clientOptions: lc.LanguageClientOptions = { - documentSelector: [{ scheme: 'file', language: 'rust' }], - }; - - client = new lc.LanguageClient( - 'ra-lsp', - 'rust-analyzer languge server', - serverOptions, - clientOptions, - ); - client.onReady().then(() => { - client.onNotification( - "m/publishDecorations", - (params: PublishDecorationsParams) => { - let editor = vscode.window.visibleTextEditors.find( - (editor) => editor.document.uri.toString() == params.uri - ) - if (!highlightingOn || !editor) return; - setHighlights( - editor, - params.decorations, - ) - } - ) - }) - client.start(); -} - -async function openDoc(uri: vscode.Uri) { - let document = await vscode.workspace.openTextDocument(uri) - return vscode.window.showTextDocument(document, vscode.ViewColumn.Two, true) -} - -class TextDocumentContentProvider implements vscode.TextDocumentContentProvider { - public eventEmitter = new vscode.EventEmitter() - public syntaxTree: string = "Not available" - - public provideTextDocumentContent(uri: vscode.Uri): vscode.ProviderResult { - let editor = vscode.window.activeTextEditor; - if (editor == null) return "" - let request: SyntaxTreeParams = { - textDocument: { uri: editor.document.uri.toString() } - }; - return client.sendRequest("m/syntaxTree", request); - } - - get onDidChange(): vscode.Event { - return this.eventEmitter.event - } -} - -let decorations: { [index: string]: vscode.TextEditorDecorationType } = {}; - -function initDecorations() { - const decor = (obj: any) => vscode.window.createTextEditorDecorationType({ color: obj }) - decorations = { - background: decor("#3F3F3F"), - error: vscode.window.createTextEditorDecorationType({ - borderColor: "red", - borderStyle: "none none dashed none", - }), - comment: decor("#7F9F7F"), - string: decor("#CC9393"), - keyword: decor("#F0DFAF"), - function: decor("#93E0E3"), - parameter: decor("#94BFF3"), - builtin: decor("#DD6718"), - text: decor("#DCDCCC"), - attribute: decor("#BFEBBF"), - literal: decor("#DFAF8F"), - } -} - -function removeHighlights() { - for (let tag in decorations) { - decorations[tag].dispose(); - } - - decorations = {}; -} - -function setHighlights( - editor: vscode.TextEditor, - highlights: Array -) { - // Initialize decorations if necessary - // - // Note: decoration objects need to be kept around so we can dispose them - // if the user disables syntax highlighting - if (Object.keys(decorations).length === 0) { - initDecorations(); - } - - let byTag: Map = new Map() - for (let tag in decorations) { - byTag.set(tag, []) - } - - for (let d of highlights) { - if (!byTag.get(d.tag)) { - console.log(`unknown tag ${d.tag}`) - continue - } - byTag.get(d.tag)!.push( - client.protocol2CodeConverter.asRange(d.range) - ) - } - - for (let tag of byTag.keys()) { - let dec: vscode.TextEditorDecorationType = decorations[tag] - let ranges = byTag.get(tag)! - editor.setDecorations(dec, ranges) - } -} - -interface SyntaxTreeParams { - textDocument: lc.TextDocumentIdentifier; -} - -type SyntaxTreeResult = string - -interface ExtendSelectionParams { - textDocument: lc.TextDocumentIdentifier; - selections: lc.Range[]; -} - -interface ExtendSelectionResult { - selections: lc.Range[]; -} - -interface FindMatchingBraceParams { - textDocument: lc.TextDocumentIdentifier; - offsets: lc.Position[]; -} - -interface JoinLinesParams { - textDocument: lc.TextDocumentIdentifier; - range: lc.Range; -} - -interface PublishDecorationsParams { - uri: string, - decorations: Decoration[], -} - -interface RunnablesParams { - textDocument: lc.TextDocumentIdentifier, - position?: lc.Position, -} - -interface Runnable { - range: lc.Range; - label: string; - bin: string; - args: string[]; - env: { [index: string]: string }, -} - -class RunnableQuickPick implements vscode.QuickPickItem { - label: string; - description?: string | undefined; - detail?: string | undefined; - picked?: boolean | undefined; - - constructor(public runnable: Runnable) { - this.label = runnable.label - } -} - -interface Decoration { - range: lc.Range, - tag: string, -} - - -interface CargoTaskDefinition extends vscode.TaskDefinition { - type: 'cargo'; - label: string; - command: string; - args: Array; - env?: { [key: string]: string }; -} - -function createTask(spec: Runnable): vscode.Task { - const TASK_SOURCE = 'Rust'; - let definition: CargoTaskDefinition = { - type: 'cargo', - label: 'cargo', - command: spec.bin, - args: spec.args, - env: spec.env - } - - let execCmd = `${definition.command} ${definition.args.join(' ')}`; - let execOption: vscode.ShellExecutionOptions = { - cwd: '.', - env: definition.env, - }; - let exec = new vscode.ShellExecution(`clear; ${execCmd}`, execOption); - - let f = vscode.workspace.workspaceFolders![0] - let t = new vscode.Task(definition, f, definition.label, TASK_SOURCE, exec, ['$rustc']); - return t; -} - -interface FileSystemEdit { - type: string; - uri?: string; - src?: string; - dst?: string; -} - -interface SourceChange { - label: string, - sourceFileEdits: lc.TextDocumentEdit[], - fileSystemEdits: FileSystemEdit[], - cursorPosition?: lc.TextDocumentPositionParams, -} - -async function applySourceChange(change: SourceChange) { - console.log(`applySOurceChange ${JSON.stringify(change)}`) - let wsEdit = new vscode.WorkspaceEdit() - for (let sourceEdit of change.sourceFileEdits) { - let uri = client.protocol2CodeConverter.asUri(sourceEdit.textDocument.uri) - let edits = client.protocol2CodeConverter.asTextEdits(sourceEdit.edits) - wsEdit.set(uri, edits) - } - let created; - let moved; - for (let fsEdit of change.fileSystemEdits) { - if (fsEdit.type == "createFile") { - let uri = vscode.Uri.parse(fsEdit.uri!) - wsEdit.createFile(uri) - created = uri - } else if (fsEdit.type == "moveFile") { - let src = vscode.Uri.parse(fsEdit.src!) - let dst = vscode.Uri.parse(fsEdit.dst!) - wsEdit.renameFile(src, dst) - moved = dst - } else { - console.error(`unknown op: ${JSON.stringify(fsEdit)}`) - } - } - let toOpen = created || moved - let toReveal = change.cursorPosition - await vscode.workspace.applyEdit(wsEdit) - if (toOpen) { - let doc = await vscode.workspace.openTextDocument(toOpen) - await vscode.window.showTextDocument(doc) - } else if (toReveal) { - let uri = client.protocol2CodeConverter.asUri(toReveal.textDocument.uri) - let position = client.protocol2CodeConverter.asPosition(toReveal.position) - let editor = vscode.window.activeTextEditor; - if (!editor || editor.document.uri.toString() != uri.toString()) return - if (!editor.selection.isEmpty) return - editor!.selection = new vscode.Selection(position, position) - } + return Server.client.stop(); } diff --git a/editors/code/src/highlighting.ts b/editors/code/src/highlighting.ts new file mode 100644 index 0000000000..e2ac4d6290 --- /dev/null +++ b/editors/code/src/highlighting.ts @@ -0,0 +1,82 @@ +import * as vscode from 'vscode'; +import * as lc from 'vscode-languageclient'; + +import { Server } from './server'; + +export interface Decoration { + range: lc.Range; + tag: string; +} + +export class Highlighter { + private static initDecorations(): Map { + const decor = (color: string) => vscode.window.createTextEditorDecorationType({ color }); + + const decorations: Iterable<[string, vscode.TextEditorDecorationType]> = [ + ['background', decor('#3F3F3F')], + ['error', vscode.window.createTextEditorDecorationType({ + borderColor: 'red', + borderStyle: 'none none dashed none', + })], + ['comment', decor('#7F9F7F')], + ['string', decor('#CC9393')], + ['keyword', decor('#F0DFAF')], + ['function', decor('#93E0E3')], + ['parameter', decor('#94BFF3')], + ['builtin', decor('#DD6718')], + ['text', decor('#DCDCCC')], + ['attribute', decor('#BFEBBF')], + ['literal', decor('#DFAF8F')], + ]; + + return new Map(decorations); + } + + private decorations: (Map | null) = null; + + public removeHighlights() { + if (this.decorations == null) { + return; + } + + // Decorations are removed when the object is disposed + for (const decoration of this.decorations.values()) { + decoration.dispose(); + } + + this.decorations = null; + } + + public setHighlights( + editor: vscode.TextEditor, + highlights: Decoration[], + ) { + // Initialize decorations if necessary + // + // Note: decoration objects need to be kept around so we can dispose them + // if the user disables syntax highlighting + if (this.decorations == null) { + this.decorations = Highlighter.initDecorations(); + } + + const byTag: Map = new Map(); + for (const tag of this.decorations.keys()) { + byTag.set(tag, []); + } + + for (const d of highlights) { + if (!byTag.get(d.tag)) { + continue; + } + byTag.get(d.tag)!.push( + Server.client.protocol2CodeConverter.asRange(d.range), + ); + } + + for (const tag of byTag.keys()) { + const dec = this.decorations.get(tag) as vscode.TextEditorDecorationType; + const ranges = byTag.get(tag)!; + editor.setDecorations(dec, ranges); + } + } +} diff --git a/editors/code/src/notifications/index.ts b/editors/code/src/notifications/index.ts new file mode 100644 index 0000000000..c565768658 --- /dev/null +++ b/editors/code/src/notifications/index.ts @@ -0,0 +1,5 @@ +import * as publishDecorations from './publish_decorations'; + +export { + publishDecorations, +}; diff --git a/editors/code/src/notifications/publish_decorations.ts b/editors/code/src/notifications/publish_decorations.ts new file mode 100644 index 0000000000..d8790386b1 --- /dev/null +++ b/editors/code/src/notifications/publish_decorations.ts @@ -0,0 +1,20 @@ +import * as vscode from 'vscode'; + +import { Decoration } from '../highlighting'; +import { Server } from '../server'; + +export interface PublishDecorationsParams { + uri: string; + decorations: Decoration[]; +} + +export function handle(params: PublishDecorationsParams) { + const targetEditor = vscode.window.visibleTextEditors.find( + (editor) => editor.document.uri.toString() === params.uri, + ); + if (!Server.config.highlightingOn || !targetEditor) { return; } + Server.highlighter.setHighlights( + targetEditor, + params.decorations, + ); +} diff --git a/editors/code/src/server.ts b/editors/code/src/server.ts new file mode 100644 index 0000000000..01fd80756f --- /dev/null +++ b/editors/code/src/server.ts @@ -0,0 +1,37 @@ +import * as lc from 'vscode-languageclient'; + +import { Config } from './config'; +import { Highlighter } from './highlighting'; + +export class Server { + public static highlighter = new Highlighter(); + public static config = new Config(); + public static client: lc.LanguageClient; + + public static start(notificationHandlers: Iterable<[string, lc.GenericNotificationHandler]>) { + const run: lc.Executable = { + command: 'ra_lsp_server', + options: { cwd: '.' }, + }; + const serverOptions: lc.ServerOptions = { + run, + debug: run, + }; + const clientOptions: lc.LanguageClientOptions = { + documentSelector: [{ scheme: 'file', language: 'rust' }], + }; + + Server.client = new lc.LanguageClient( + 'ra-lsp', + 'rust-analyzer languge server', + serverOptions, + clientOptions, + ); + Server.client.onReady().then(() => { + for (const [type, handler] of notificationHandlers) { + Server.client.onNotification(type, handler); + } + }); + Server.client.start(); + } +} diff --git a/editors/code/tslint.json b/editors/code/tslint.json new file mode 100644 index 0000000000..ce48dfc95e --- /dev/null +++ b/editors/code/tslint.json @@ -0,0 +1,13 @@ +{ + "defaultSeverity": "error", + "extends": [ + "tslint:recommended" + ], + "jsRules": {}, + "rules": { + "quotemark": [true, "single"], + "interface-name": false, + "object-literal-sort-keys": false + }, + "rulesDirectory": [] +}