Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: update rename functionality #141

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
51 changes: 50 additions & 1 deletion lib/adapters/rename-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -34,6 +46,36 @@ export default class RenameAdapter {
}
}

public static async rename(
connection: LanguageClientConnection,
editor: TextEditor,
point: Point,
newName: string
): Promise<ApplyWorkspaceEditResponse> {
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),
Expand All @@ -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<atomIde.IdeUri, atomIde.TextEdit[]> {
const result = new Map()
Object.keys(changes).forEach((uri) => {
Expand Down
100 changes: 99 additions & 1 deletion lib/auto-languageclient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,9 @@ 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'

export { ActiveServer, LanguageClientConnection, LanguageServerProcess }
export type ConnectionType = "stdio" | "socket" | "ipc"
Expand Down Expand Up @@ -218,6 +219,7 @@ export default class AutoLanguageClient {
dynamicRegistration: false,
},
rename: {
prepareSupport: true,
dynamicRegistration: false,
},
moniker: {
Expand Down Expand Up @@ -308,6 +310,7 @@ export default class AutoLanguageClient {
this.getServerName()
)
this._serverManager.startListening()
this.registerRenameCommands()
process.on("exit", () => this.exitCleanup.bind(this))
}

Expand Down Expand Up @@ -890,6 +893,101 @@ 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<TextEditorElement>) => {
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)
}
}
]
}))
}

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,
Expand Down
16 changes: 16 additions & 0 deletions lib/languageclient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<lsp.Range | { range: lsp.Range, placeholder: string } | { defaultBehavior: boolean } | null> {
return this._sendRequest("textDocument/prepareRename", params)
}

/**
* Public: Send a `textDocument/rename` request.
*
Expand Down
2 changes: 2 additions & 0 deletions lib/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -21,4 +22,5 @@ export {
DownloadFile,
LinterPushV2Adapter,
CommandExecutionAdapter,
RenameAdapter
}
37 changes: 37 additions & 0 deletions lib/views/dialog.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { TextEditor } from "atom"

export default class Dialog {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually created a dialog view for some other atom packages https://github.com/UziTech/atom-modal-views. Do we want to use that instead of creating our own?

/cc @aminya

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually created a dialog view for some other atom packages UziTech/atom-modal-views. Do we want to use that instead of creating our own?

That sounds like a good plan! I love reusing code.

public static async prompt (message: string): Promise<string>
{
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()
})

})
}
}