Skip to content

Commit efdfc69

Browse files
authored
Merge pull request #135 from atom-community/window/showDocument
2 parents 3cd9630 + 9692643 commit efdfc69

File tree

6 files changed

+199
-0
lines changed

6 files changed

+199
-0
lines changed

lib/adapters/show-document-adapter.ts

+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { shell } from "electron"
2+
import { LanguageClientConnection, ShowDocumentParams, ShowDocumentResult } from "../languageclient"
3+
import { TextEditor } from "atom"
4+
import Convert from "../convert"
5+
6+
/** Public: Adapts the window/showDocument command to Atom's text editors or external programs. */
7+
const ShowDocumentAdapter = {
8+
/** {@inheritDoc attach} */
9+
attach,
10+
/** {@inheritDoc showDocument} */
11+
showDocument,
12+
}
13+
// for consistency with other adapters
14+
export default ShowDocumentAdapter
15+
16+
/**
17+
* Public: Attach to a {LanguageClientConnection} to recieve requests to show documents.
18+
*/
19+
export function attach(connection: LanguageClientConnection): void {
20+
connection.onShowDocument(showDocument)
21+
}
22+
23+
/**
24+
* Public: show documents inside Atom text editor or in external programs
25+
*
26+
* @param params The {ShowDocumentParams} received from the language server
27+
* indicating the document to be displayed as well as other metadata.
28+
* @returns {Promise<ShowDocumentResult>} with a `success: boolean` property specifying if the operation was sucessful
29+
* {@inheritDoc ShowDocumentParams}
30+
*/
31+
export async function showDocument(params: ShowDocumentParams): Promise<ShowDocumentResult> {
32+
try {
33+
if (!params.external) {
34+
// open using atom.workspace
35+
const view = await atom.workspace.open(Convert.uriToPath(params.uri), {
36+
activateItem: params.takeFocus,
37+
activatePane: params.takeFocus,
38+
pending: true,
39+
initialLine: params.selection?.start.line ?? 0,
40+
initialColumn: params.selection?.start.character ?? 0,
41+
})
42+
if (!view) {
43+
return { success: false }
44+
}
45+
if (view instanceof TextEditor && params.selection !== undefined) {
46+
view.selectToBufferPosition(Convert.positionToPoint(params.selection.end))
47+
}
48+
} else {
49+
// open using Electron
50+
shell.openExternal(params.uri, { activate: params.takeFocus })
51+
}
52+
return { success: true }
53+
} catch (e) {
54+
atom.notifications.addError(e)
55+
return { success: false }
56+
}
57+
}

lib/auto-languageclient.ts

+8
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import NotificationsAdapter from "./adapters/notifications-adapter"
2121
import OutlineViewAdapter from "./adapters/outline-view-adapter"
2222
import RenameAdapter from "./adapters/rename-adapter"
2323
import SignatureHelpAdapter from "./adapters/signature-help-adapter"
24+
import * as ShowDocumentAdapter from "./adapters/show-document-adapter"
2425
import * as Utils from "./utils"
2526
import { Socket } from "net"
2627
import { LanguageClientConnection } from "./languageclient"
@@ -245,6 +246,11 @@ export default class AutoLanguageClient {
245246
regularExpressions: undefined,
246247
markdown: undefined,
247248
},
249+
window: {
250+
workDoneProgress: false, // TODO: support
251+
showMessage: undefined,
252+
showDocument: { support: true },
253+
},
248254
experimental: {},
249255
},
250256
}
@@ -554,6 +560,8 @@ export default class AutoLanguageClient {
554560
loggingConsole,
555561
signatureHelpAdapter,
556562
})
563+
564+
ShowDocumentAdapter.attach(server.connection)
557565
}
558566

559567
public shouldSyncForEditor(editor: TextEditor, projectPath: string): boolean {

lib/languageclient.ts

+11
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export interface KnownNotifications {
1515
}
1616

1717
export interface KnownRequests {
18+
"window/showDocument": [lsp.ShowDocumentParams, lsp.ShowDocumentResult]
1819
"window/showMessageRequest": [lsp.ShowMessageRequestParams, lsp.MessageActionItem | null]
1920
"workspace/applyEdit": [lsp.ApplyWorkspaceEditParams, lsp.ApplyWorkspaceEditResponse]
2021
[custom: string]: [Record<string, any>, Record<string, any> | null]
@@ -166,6 +167,16 @@ export class LanguageClientConnection extends EventEmitter {
166167
this._onRequest({ method: "window/showMessageRequest" }, callback)
167168
}
168169

170+
/**
171+
* Public: Register a callback for the `window/showDocument` message.
172+
*
173+
* @param callback The function to be called when the `window/showDocument` message is
174+
* received with {ShowDocumentParams} being passed.
175+
*/
176+
public onShowDocument(callback: (params: lsp.ShowDocumentParams) => Promise<lsp.ShowDocumentResult>): void {
177+
this._onRequest({ method: "window/showDocument" }, callback)
178+
}
179+
169180
/**
170181
* Public: Register a callback for the `window/logMessage` message.
171182
*
+112
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import { TextEditor } from "atom"
2+
import { shell } from "electron"
3+
import * as sinon from "sinon"
4+
import { expect } from "chai"
5+
import { join, dirname } from "path"
6+
import * as ShowDocumentAdapter from "../../lib/adapters/show-document-adapter"
7+
import { LanguageClientConnection, ShowDocumentParams } from "../../lib/languageclient"
8+
import Convert from "../../lib/convert"
9+
import { createSpyConnection } from "../helpers"
10+
11+
describe("ShowDocumentAdapter", () => {
12+
describe("can attach to a server", () => {
13+
it("subscribes to onShowDocument", async () => {
14+
const connection = createSpyConnection()
15+
const lcc = new LanguageClientConnection(connection)
16+
17+
const spy = sinon.spy()
18+
lcc["_onRequest"] = spy
19+
20+
ShowDocumentAdapter.attach(lcc)
21+
expect((lcc["_onRequest"] as sinon.SinonSpy).calledOnce).to.be.true
22+
const spyArgs = spy.firstCall.args
23+
expect(spyArgs[0]).to.deep.equal({ method: "window/showDocument" })
24+
expect(spyArgs[1]).to.equal(ShowDocumentAdapter.showDocument)
25+
})
26+
27+
it("onRequest connection is called", async () => {
28+
const connection = createSpyConnection()
29+
const lcc = new LanguageClientConnection(connection)
30+
31+
const spy = sinon.spy()
32+
connection.onRequest = spy
33+
34+
ShowDocumentAdapter.attach(lcc)
35+
expect((connection.onRequest as sinon.SinonSpy).calledOnce).to.be.true
36+
const spyArgs = spy.firstCall.args
37+
expect(spyArgs[0]).to.equal("window/showDocument")
38+
expect(typeof spyArgs[1]).to.equal("function")
39+
})
40+
})
41+
describe("can show documents", () => {
42+
describe("shows the document inside Atom", async () => {
43+
const helloPath = join(dirname(__dirname), "fixtures", "hello.js")
44+
45+
async function canShowDocumentInAtom(params: ShowDocumentParams) {
46+
const { success } = await ShowDocumentAdapter.showDocument(params)
47+
expect(success).to.be.true
48+
49+
const editor = atom.workspace.getTextEditors()[0]
50+
expect(editor instanceof TextEditor).to.be.true
51+
52+
expect(editor!.getPath()).includes(helloPath)
53+
expect(editor!.getText()).includes(`atom.notifications.addSuccess("Hello World")`)
54+
55+
return editor
56+
}
57+
58+
beforeEach(() => {
59+
atom.workspace.getTextEditors().forEach((ed) => {
60+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
61+
//@ts-ignore
62+
ed.destroy()
63+
})
64+
})
65+
66+
it("shows the document inside Atom for the given URI", async () => {
67+
const params: ShowDocumentParams = {
68+
uri: helloPath,
69+
}
70+
await canShowDocumentInAtom(params)
71+
})
72+
73+
it("takes the focus", async () => {
74+
const params: ShowDocumentParams = {
75+
uri: helloPath,
76+
takeFocus: true,
77+
}
78+
const editor = await canShowDocumentInAtom(params)
79+
expect(atom.workspace.getActivePane()?.getItems()[0]).equal(editor)
80+
})
81+
82+
it("selects the given selection range", async () => {
83+
const selectionLSRange = { start: { line: 1, character: 30 }, end: { line: 1, character: 43 } } // `"Hello World"` in JavaScript file
84+
const params: ShowDocumentParams = {
85+
uri: helloPath,
86+
selection: selectionLSRange,
87+
}
88+
const editor = await canShowDocumentInAtom(params)
89+
expect(editor.getSelectedBufferRange()).deep.equal(Convert.lsRangeToAtomRange(selectionLSRange))
90+
expect(editor.getSelectedText()).equal(`"Hello World"`)
91+
})
92+
})
93+
94+
describe("shows document in external programs", () => {
95+
it("shows the document in external programs for the given URI", async () => {
96+
const params: ShowDocumentParams = {
97+
uri: "http://www.github.com",
98+
external: true,
99+
}
100+
const spy = sinon.spy()
101+
shell.openExternal = spy
102+
103+
const { success } = await ShowDocumentAdapter.showDocument(params)
104+
expect(success).to.be.true
105+
106+
expect((shell.openExternal as sinon.SinonSpy).calledOnce).to.be.true
107+
const spyArgs = spy.firstCall.args
108+
expect(spyArgs[0]).to.equal("http://www.github.com")
109+
})
110+
})
111+
})
112+
})

test/fixtures/hello.ts

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
atom.notifications.addSuccess("Hello World")

typings/electron/index.d.ts

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
interface OpenExternalOptions {
2+
activate?: boolean
3+
workingDirectory?: string
4+
}
5+
6+
declare module "electron" {
7+
export const shell: {
8+
openExternal(url: string, options?: OpenExternalOptions): void
9+
}
10+
}

0 commit comments

Comments
 (0)