From fa14f7036c6b7f4bd469a1b51081ee524b0440d6 Mon Sep 17 00:00:00 2001 From: Christian Daguerre Date: Sun, 11 Apr 2021 10:13:54 +0200 Subject: [PATCH 1/4] feat: add prepare rename support and expose adapter --- lib/adapters/rename-adapter.ts | 51 +++++++++++++++++++++++++++++++++- lib/auto-languageclient.ts | 1 + lib/languageclient.ts | 16 +++++++++++ lib/main.ts | 2 ++ 4 files changed, 69 insertions(+), 1 deletion(-) diff --git a/lib/adapters/rename-adapter.ts b/lib/adapters/rename-adapter.ts index d87d6fc9..219f1b1f 100644 --- a/lib/adapters/rename-adapter.ts +++ b/lib/adapters/rename-adapter.ts @@ -3,15 +3,27 @@ import Convert from "../convert" import { Point, TextEditor } from "atom" import { LanguageClientConnection, + PrepareRenameParams, RenameParams, ServerCapabilities, TextDocumentEdit, + ApplyWorkspaceEditResponse, TextEdit, + Range } from "../languageclient" +import ApplyEditAdapter from "./apply-edit-adapter" export default class RenameAdapter { public static canAdapt(serverCapabilities: ServerCapabilities): boolean { - return serverCapabilities.renameProvider === true + return serverCapabilities.renameProvider !== false + } + + public static canPrepare(serverCapabilities: ServerCapabilities): boolean { + if (serverCapabilities.renameProvider === undefined || typeof serverCapabilities.renameProvider === 'boolean') { + return false + } + + return serverCapabilities.renameProvider.prepareProvider || false } public static async getRename( @@ -34,6 +46,36 @@ export default class RenameAdapter { } } + public static async rename( + connection: LanguageClientConnection, + editor: TextEditor, + point: Point, + newName: string + ): Promise { + const edit = await connection.rename(RenameAdapter.createRenameParams(editor, point, newName)) + return ApplyEditAdapter.onApplyEdit({ edit }) + } + + public static async prepareRename( + connection: LanguageClientConnection, + editor: TextEditor, + point: Point, + ): Promise<{ possible: boolean, range?: Range, label?: string | null}> { + const result = await connection.prepareRename(RenameAdapter.createPrepareRenameParams(editor, point)) + + if (!result) { + return { possible: false } + } + if ('defaultBehavior' in result) { + return { possible: result.defaultBehavior } + } + return { + possible: true, + range: 'range' in result ? result.range : result, + label: 'range' in result ? result.placeholder : null + } + } + public static createRenameParams(editor: TextEditor, point: Point, newName: string): RenameParams { return { textDocument: Convert.editorToTextDocumentIdentifier(editor), @@ -42,6 +84,13 @@ export default class RenameAdapter { } } + public static createPrepareRenameParams(editor: TextEditor, point: Point): PrepareRenameParams { + return { + textDocument: Convert.editorToTextDocumentIdentifier(editor), + position: Convert.pointToPosition(point), + } + } + public static convertChanges(changes: { [uri: string]: TextEdit[] }): Map { const result = new Map() Object.keys(changes).forEach((uri) => { diff --git a/lib/auto-languageclient.ts b/lib/auto-languageclient.ts index 62eed42b..c4495e50 100644 --- a/lib/auto-languageclient.ts +++ b/lib/auto-languageclient.ts @@ -218,6 +218,7 @@ export default class AutoLanguageClient { dynamicRegistration: false, }, rename: { + prepareSupport: true, dynamicRegistration: false, }, moniker: { diff --git a/lib/languageclient.ts b/lib/languageclient.ts index efebe817..298868e1 100644 --- a/lib/languageclient.ts +++ b/lib/languageclient.ts @@ -494,6 +494,22 @@ export class LanguageClientConnection extends EventEmitter { return this._sendRequest("textDocument/onTypeFormatting", params) } + /** + * Public: Send a `textDocument/prepareRename` request. + * + * @param params The {PrepareRenameParams} identifying the document containing the symbol to be renamed, + * as well as the position. + * @returns A {Promise} containing either: + * - a {Range} of the string to rename and optionally a `placeholder` text of the string content to + * be renamed. + * - `{ defaultBehavior: boolean }` is returned (since 3.16) if the rename position is valid and the client + * should use its default behavior to compute the rename range. + * - `null` is returned when it is deemed that a ‘textDocument/rename’ request is not valid at the given position + */ + public prepareRename(params: lsp.PrepareRenameParams): Promise { + return this._sendRequest("textDocument/prepareRename", params) + } + /** * Public: Send a `textDocument/rename` request. * diff --git a/lib/main.ts b/lib/main.ts index 3b16d2e1..3d5945c6 100644 --- a/lib/main.ts +++ b/lib/main.ts @@ -9,6 +9,7 @@ import { Logger, ConsoleLogger, FilteredLogger } from "./logger" import DownloadFile from "./download-file" import LinterPushV2Adapter from "./adapters/linter-push-v2-adapter" import CommandExecutionAdapter from "./adapters/command-execution-adapter" +import RenameAdapter from "./adapters/rename-adapter" export { getExePath } from "./utils" export * from "./auto-languageclient" @@ -21,4 +22,5 @@ export { DownloadFile, LinterPushV2Adapter, CommandExecutionAdapter, + RenameAdapter } From 89f4401af1ba4dd3b4fec792f88ceaf0ccdc6863 Mon Sep 17 00:00:00 2001 From: Christian Daguerre Date: Mon, 12 Apr 2021 18:05:25 +0200 Subject: [PATCH 2/4] feat: add prompt dialog --- lib/auto-languageclient.ts | 1 + lib/views/dialog.ts | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 lib/views/dialog.ts diff --git a/lib/auto-languageclient.ts b/lib/auto-languageclient.ts index c4495e50..7264acdd 100644 --- a/lib/auto-languageclient.ts +++ b/lib/auto-languageclient.ts @@ -29,6 +29,7 @@ import { ConsoleLogger, FilteredLogger, Logger } from "./logger" import { LanguageServerProcess, ServerManager, ActiveServer } from "./server-manager.js" import { Disposable, CompositeDisposable, Point, Range, TextEditor } from "atom" import * as ac from "atom/autocomplete-plus" +import Dialog from './views/dialog' export { ActiveServer, LanguageClientConnection, LanguageServerProcess } export type ConnectionType = "stdio" | "socket" | "ipc" diff --git a/lib/views/dialog.ts b/lib/views/dialog.ts new file mode 100644 index 00000000..47eccb28 --- /dev/null +++ b/lib/views/dialog.ts @@ -0,0 +1,37 @@ +import { TextEditor } from "atom" + +export default class Dialog { + public static async prompt (message: string): Promise + { + const miniEditor = new TextEditor({ mini: true }) + const editorElement = atom.views.getView(miniEditor) + + const messageElement = document.createElement('div') + messageElement.classList.add('message') + messageElement.textContent = message + + const element = document.createElement('div') + element.classList.add('prompt') + element.appendChild(editorElement) + element.appendChild(messageElement) + + const panel = atom.workspace.addModalPanel({ + item: element, + visible: true + }) + + editorElement.focus() + + return new Promise((resolve, reject) => { + atom.commands.add(editorElement, 'core:confirm', () => { + resolve(miniEditor.getText()) + panel.destroy() + }) + atom.commands.add(editorElement, 'core:cancel', () => { + reject() + panel.destroy() + }) + + }) + } +} \ No newline at end of file From 0516e36b087683b522d7166fae8edea89407e7c8 Mon Sep 17 00:00:00 2001 From: Christian Daguerre Date: Mon, 12 Apr 2021 18:07:45 +0200 Subject: [PATCH 3/4] feat: implement command based rename --- lib/auto-languageclient.ts | 58 +++++++++++++++++++++++++++++++++++++- 1 file changed, 57 insertions(+), 1 deletion(-) diff --git a/lib/auto-languageclient.ts b/lib/auto-languageclient.ts index 7264acdd..437bce97 100644 --- a/lib/auto-languageclient.ts +++ b/lib/auto-languageclient.ts @@ -27,7 +27,7 @@ import { Socket } from "net" import { LanguageClientConnection } from "./languageclient" import { ConsoleLogger, FilteredLogger, Logger } from "./logger" import { LanguageServerProcess, ServerManager, ActiveServer } from "./server-manager.js" -import { Disposable, CompositeDisposable, Point, Range, TextEditor } from "atom" +import { Disposable, CompositeDisposable, Point, Range, TextEditor, CommandEvent, TextEditorElement } from "atom" import * as ac from "atom/autocomplete-plus" import Dialog from './views/dialog' @@ -310,6 +310,7 @@ export default class AutoLanguageClient { this.getServerName() ) this._serverManager.startListening() + this.registerRenameCommands() process.on("exit", () => this.exitCleanup.bind(this)) } @@ -892,6 +893,61 @@ export default class AutoLanguageClient { rename: this.getRename.bind(this), } } + + public async registerRenameCommands() + { + this._disposable.add(atom.commands.add('atom-text-editor', 'IDE:Rename', async (event: CommandEvent) => { + const textEditorElement = event.currentTarget + const textEditor = textEditorElement.getModel() + const bufferPosition = textEditor.getCursorBufferPosition() + const server = await this._serverManager.getServer(textEditor) + + if (!server) { + return + } + + if (!RenameAdapter.canAdapt(server.capabilities)) { + atom.notifications.addInfo(`Rename is not supported by ${this.getServerName()}`) + } + + const outcome = { possible: true, label: 'Rename' } + if (RenameAdapter.canPrepare(server.capabilities)) { + const { possible } = await RenameAdapter.prepareRename(server.connection, textEditor, bufferPosition) + outcome.possible = possible + } + + if (!outcome.possible) { + atom.notifications.addWarning(`Nothing to rename at position at row ${bufferPosition.row+1} and column ${bufferPosition.column+1}`) + return; + } + const newName = await Dialog.prompt('Enter new name') + RenameAdapter.rename(server.connection, textEditor, bufferPosition, newName) + return + })) + + this._disposable.add(atom.contextMenu.add({ + 'atom-text-editor': [ + { + label: 'Refactor', + submenu: [ + { label: "Rename", command: "IDE:Rename" } + ], + created: function (event: MouseEvent) { + const textEditor = atom.workspace.getActiveTextEditor() + if (!textEditor) { + return + } + + const screenPosition = atom.views.getView(textEditor).getComponent().screenPositionForMouseEvent(event) + const bufferPosition = textEditor.bufferPositionForScreenPosition(screenPosition) + + textEditor.setCursorBufferPosition(bufferPosition) + } + } + ] + })) + } + protected async getRename( editor: TextEditor, From 35f957e0ce60c61ef0d4d405a02b8948058f048b Mon Sep 17 00:00:00 2001 From: Christian Daguerre Date: Mon, 12 Apr 2021 18:09:23 +0200 Subject: [PATCH 4/4] feat: implement intentions based rename --- lib/auto-languageclient.ts | 40 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/lib/auto-languageclient.ts b/lib/auto-languageclient.ts index 437bce97..161f4a93 100644 --- a/lib/auto-languageclient.ts +++ b/lib/auto-languageclient.ts @@ -948,6 +948,46 @@ export default class AutoLanguageClient { })) } + public provideIntentions() { + return { + grammarScopes: this.getGrammarScopes(), // [*] would also work + getIntentions: async ({ textEditor, bufferPosition }: { textEditor: TextEditor; bufferPosition: Point }) => { + const intentions: { title: string, selected: () => void }[] = [] + const server = await this._serverManager.getServer(textEditor) + + if (server == null) { + return intentions + } + + if (RenameAdapter.canAdapt(server.capabilities)) { + const outcome = { possible: true, label: 'Rename' } + if (RenameAdapter.canPrepare(server.capabilities)) { + const { possible } = await RenameAdapter.prepareRename(server.connection, textEditor, bufferPosition) + outcome.possible = possible + } + + if (outcome.possible) { + intentions.push({ + title: outcome.label, + selected: async () => { + const newName = await Dialog.prompt('Enter new name') + return RenameAdapter.rename(server.connection, textEditor, bufferPosition, newName) + } + }) + } + } + + intentions.push({ + title: 'Some dummy intention', + selected: async () => { + console.log('selected') + } + }) + + return intentions + } + } + } protected async getRename( editor: TextEditor,