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

Implement signature intellisense #640

Merged
merged 13 commits into from
Dec 3, 2021
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion assets/css/components.css
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
@layer components {
/* Buttons */

.button {
.button-base {
@apply px-5 py-2 rounded-lg border border-transparent font-medium text-sm;
}

Expand Down
71 changes: 57 additions & 14 deletions assets/css/editor.css
Original file line number Diff line number Diff line change
Expand Up @@ -6,54 +6,78 @@ so we need to adjust styles of Monaco-rendered Markdown docs.
Also some spacing adjustments.
*/

.suggest-details .header p {
@apply pb-0 pt-3 !important;
}

.monaco-hover p,
.suggest-details .docs p {
.suggest-details p,
.parameter-hints-widget p {
@apply my-2 !important;
}

.suggest-details h1,
.monaco-hover h1 {
.monaco-hover h1,
.parameter-hints-widget h1 {
@apply text-xl font-semibold mt-4 mb-2;
}

.suggest-details h2,
.monaco-hover h2 {
.monaco-hover h2,
.parameter-hints-widget h2 {
@apply text-lg font-medium mt-4 mb-2;
}

.suggest-details h3,
.monaco-hover h3 {
.monaco-hover h3,
.parameter-hints-widget h3 {
@apply font-medium mt-4 mb-2;
}

.suggest-details ul,
.monaco-hover ul {
.monaco-hover ul,
.parameter-hints-widget ul {
@apply list-disc;
}

.suggest-details ol,
.monaco-hover ol {
.monaco-hover ol,
.parameter-hints-widget ol {
@apply list-decimal;
}

.suggest-details hr,
.monaco-hover hr {
.monaco-hover hr,
.parameter-hints-widget hr {
@apply my-2 !important;
}

.suggest-details blockquote,
.monaco-hover blockquote {
.monaco-hover blockquote,
.parameter-hints-widget blockquote {
@apply border-l-4 border-gray-200 pl-4 py-0.5 my-2;
}

/* Add some spacing to code snippets in completion suggestions */
.suggest-details div.monaco-tokenized-source,
.monaco-hover div.monaco-tokenized-source {
@apply my-2;
.monaco-hover div.monaco-tokenized-source,
.parameter-hints-widget div.monaco-tokenized-source {
@apply my-2 whitespace-pre-wrap;
}

/* Use z-index over cell icons */
.suggest-details,
.monaco-hover,
.parameter-hints-widget {
z-index: 100 !important;
}

/* Adjust header spacing in completion details */
.suggest-details .header p {
@apply pb-0 pt-3 !important;
}

/* Adjust divider in signature help widget */
.parameter-hints-widget .markdown-docs hr {
border-top: 1px solid rgba(69, 69, 69, 0.5);
margin-right: -8px;
margin-left: -8px;
}

/* Increase the hover box limits */
Expand All @@ -62,6 +86,25 @@ Also some spacing adjustments.
max-height: 300px !important;
}

/* Increase the completion details box limits */
.suggest-details-container,
.suggest-details {
width: fit-content !important;
height: fit-content !important;
max-width: 420px !important;
max-height: 250px !important;
}

/* Adjust completion details spacing */
.suggest-details .header .type {
padding-top: 0 !important;
}

/* The content already has some padding */
.docs.markdown-docs {
margin: 0 !important;
}

/* === Monaco cursor widget === */

.monaco-cursor-widget-container {
Expand Down
63 changes: 62 additions & 1 deletion assets/js/cell/live_editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ import EditorClient from "./live_editor/editor_client";
import MonacoEditorAdapter from "./live_editor/monaco_editor_adapter";
import HookServerAdapter from "./live_editor/hook_server_adapter";
import RemoteUser from "./live_editor/remote_user";
import { replacedSuffixLength } from "../highlight/text_utils";
import {
replacedSuffixLength,
functionCallCodeUntilCursor,
} from "../lib/text_utils";

/**
* Mounts cell source editor with real-time collaboration mechanism.
Expand Down Expand Up @@ -164,6 +167,7 @@ class LiveEditor {
quickSuggestions: false,
tabCompletion: "on",
suggestSelection: "first",
parameterHints: true,
});

this.editor.getModel().updateOptions({
Expand Down Expand Up @@ -291,6 +295,46 @@ class LiveEditor {
.catch(() => null);
};

const signatureCache = {
codeUntilLastStop: null,
response: null,
};

this.editor.getModel().__getSignatureHelp = (model, position) => {
// Use a heuristic to support multiline calls, without sending all the code
jonatanklosko marked this conversation as resolved.
Show resolved Hide resolved
const codeUntilCursor = functionCallCodeUntilCursor(
model.getLinesContent(),
position.lineNumber - 1,
position.column
);

// Remove trailing characters that don't affect the signature
const codeUntilLastStop = codeUntilCursor.replace(/[^(),]*?$/, "");

// Cache subsequent requests for the same prefix, so that we don't
// make unnecessary requests
if (codeUntilLastStop === signatureCache.codeUntilLastStop) {
return {
value: signatureResponseToSignatureHelp(signatureCache.response),
dispose: () => {},
};
}

return this.__asyncIntellisenseRequest("signature", {
hint: codeUntilCursor,
})
.then((response) => {
signatureCache.response = response;
signatureCache.codeUntilLastStop = codeUntilLastStop;

return {
value: signatureResponseToSignatureHelp(response),
dispose: () => {},
};
})
.catch(() => null);
};

this.editor.getModel().__getDocumentFormattingEdits = (model) => {
const content = model.getValue();

Expand Down Expand Up @@ -416,4 +460,21 @@ function numberToSortableString(number, maxNumber) {
return String(number).padStart(maxNumber, "0");
}

function signatureResponseToSignatureHelp(response) {
return {
activeSignature: 0,
activeParameter: response.active_argument,
signatures: response.signature_items.map((signature_item) => ({
label: signature_item.signature,
parameters: signature_item.arguments.map((argument) => ({
label: argument,
})),
documentation: signature_item.documentation && {
value: signature_item.documentation,
isTrusted: true,
},
})),
};
}

export default LiveEditor;
11 changes: 11 additions & 0 deletions assets/js/cell/live_editor/monaco.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,17 @@ monaco.languages.registerHoverProvider("elixir", {
},
});

monaco.languages.registerSignatureHelpProvider("elixir", {
signatureHelpTriggerCharacters: ["(", ","],
provideSignatureHelp: (model, position, token, context) => {
if (model.__getSignatureHelp) {
return model.__getSignatureHelp(model, position);
} else {
return null;
}
},
});

monaco.languages.registerDocumentFormattingEditProvider("elixir", {
provideDocumentFormattingEdits: (model, options, token) => {
if (model.__getDocumentFormattingEdits) {
Expand Down
13 changes: 0 additions & 13 deletions assets/js/highlight/text_utils.js

This file was deleted.

49 changes: 49 additions & 0 deletions assets/js/lib/text_utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/**
* Returns length of suffix in `string` that should be replaced
* with `newSuffix` to avoid duplication.
*/
export function replacedSuffixLength(string, newSuffix) {
let suffix = newSuffix;

while (!string.endsWith(suffix)) {
suffix = suffix.slice(0, -1);
}

return suffix.length;
}

/**
* Given lines and cursor position returns a code snippet with
* unclosed parenthesised call.
*
* If there is no unclosed call, the current line is returned
* instead.
*/
export function functionCallCodeUntilCursor(
lines,
cursorLineIdx,
cursorColumn
) {
const currentLine = lines[cursorLineIdx].slice(0, cursorColumn - 1);

let openings = characterCount(currentLine, "(");
let closings = characterCount(currentLine, ")");
let lineIdx = cursorLineIdx;

while (lineIdx > 0 && openings <= closings) {
lineIdx--;
const line = lines[lineIdx];
openings += characterCount(line, "(");
closings += characterCount(line, ")");
}

if (openings > closings) {
return lines.slice(lineIdx, cursorLineIdx).concat([currentLine]).join("\n");
} else {
return currentLine;
}
}

function characterCount(string, char) {
return string.split(char).length - 1;
}
15 changes: 9 additions & 6 deletions assets/js/session/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -293,15 +293,18 @@ function handleDocumentKeyDown(hook, event) {
if (key === "Escape") {
const monacoInputOpen = !!event.target.closest(".monaco-inputbox");

const activeDescendant = event.target.getAttribute(
"aria-activedescendant"
const editor = event.target.closest(".monaco-editor.focused");

const completionBoxOpen = !!editor.querySelector(
".editor-widget.parameter-hints-widget.visible"
);
const signatureDetailsOpen = !!editor.querySelector(
".editor-widget.suggest-widget.visible"
);
const completionBoxOpen =
activeDescendant && activeDescendant.includes("suggest");

// Ignore Escape if it's supposed to close some Monaco input
// (like the find/replace box), or the completion box.
if (!monacoInputOpen && !completionBoxOpen) {
// (like the find/replace box), or an intellisense widget
if (!monacoInputOpen && !completionBoxOpen && !signatureDetailsOpen) {
escapeInsertMode(hook);
}
} else if (cmd && key === "Enter" && !alt) {
Expand Down
33 changes: 32 additions & 1 deletion assets/test/lib/text_utils.test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { replacedSuffixLength } from "../../js/highlight/text_utils";
import {
functionCallCodeUntilCursor,
replacedSuffixLength,
} from "../../js/lib/text_utils";

test("replacedSuffixLength", () => {
expect(replacedSuffixLength("to_string(", "")).toEqual(0);
Expand All @@ -8,3 +11,31 @@ test("replacedSuffixLength", () => {
expect(replacedSuffixLength("Enum.ma", "map")).toEqual(2);
expect(replacedSuffixLength("Enum.ma", "map_reduce")).toEqual(2);
});

describe("functionCallCodeUntilCursor", () => {
test("unclosed call", () => {
const lines = ["Enum.map([1, 2], )"];
expect(functionCallCodeUntilCursor(lines, 0, 13)).toEqual("Enum.map([1,");
expect(functionCallCodeUntilCursor(lines, 0, 17)).toEqual(
"Enum.map([1, 2],"
);
});

test("unclosed multiline call", () => {
const lines = ["Enum.map(", " [1, 2],", " fn", ")"];
expect(functionCallCodeUntilCursor(lines, 0, 10)).toEqual("Enum.map(");
expect(functionCallCodeUntilCursor(lines, 1, 6)).toEqual(
"Enum.map(\n [1,"
);
expect(functionCallCodeUntilCursor(lines, 2, 4)).toEqual(
"Enum.map(\n [1, 2],\n f"
);
});

test("returns a single line when all cells are closed", () => {
const lines = ["Enum.map([1, 2], fun)", "length([])", "length []"];
expect(functionCallCodeUntilCursor(lines, 1, 4)).toEqual("len");
expect(functionCallCodeUntilCursor(lines, 1, 11)).toEqual("length([])");
expect(functionCallCodeUntilCursor(lines, 2, 10)).toEqual("length []");
});
});
Loading