Skip to content

Commit cd7f0e0

Browse files
committed
working towards inline completions
1 parent bc29d10 commit cd7f0e0

File tree

6 files changed

+364
-4
lines changed

6 files changed

+364
-4
lines changed

LSP-copilot.sublime-commands

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,12 @@
77
"default": "// Settings in here override those in \"LSP-copilot/LSP-copilot.sublime-settings\"\n\n{\n\t$0\n}\n",
88
},
99
},
10+
{
11+
"caption": "Copilot: Inline Completion with Prompt",
12+
"command": "copilot_inline_completion_prompt",
13+
},
14+
{
15+
"caption": "Copilot: Inline Completion",
16+
"command": "copilot_inline_completion",
17+
},
1018
]

Main.sublime-commands

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33
"caption": "Copilot: Chat",
44
"command": "copilot_conversation_chat"
55
},
6+
{
7+
"caption": "CopilotSelectInlineCompletionCommand",
8+
"command": "copilot_select_inline_completion"
9+
},
610
{
711
"caption": "Copilot: Code Review",
812
"command": "copilot_code_review"

boot.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ def reload_plugin() -> None:
1010
del sys.modules[module_name]
1111

1212

13+
1314
reload_plugin()
1415

1516
from .plugin import * # noqa: E402, F403

plugin/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@
1919
CopilotConversationDestroyCommand,
2020
CopilotConversationDestroyShimCommand,
2121
CopilotGitCommitGenerateCommand,
22+
CopilotInlineCompletionPromptCommand,
23+
CopilotInlineCompletionCommand,
24+
CopilotSelectInlineCompletionCommand,
2225
CopilotConversationInsertCodeCommand,
2326
CopilotConversationInsertCodeShimCommand,
2427
CopilotConversationRatingCommand,
@@ -61,10 +64,13 @@
6164
"CopilotCheckFileStatusCommand",
6265
"CopilotCheckStatusCommand",
6366
"CopilotCodeReviewCommand",
67+
"CopilotInlineCompletionPromptCommand",
6468
"CopilotEditConversationCreateCommand",
6569
"CopilotEditConversationDestroyShimCommand",
6670
"CopilotEditConversationDestroyCommand",
6771
"CopilotClosePanelCompletionCommand",
72+
"CopilotInlineCompletionCommand",
73+
"CopilotSelectInlineCompletionCommand",
6874
"CopilotConversationAgentsCommand",
6975
"CopilotConversationChatCommand",
7076
"CopilotConversationChatShimCommand",

plugin/commands.py

Lines changed: 323 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@
4949
REQ_GET_PANEL_COMPLETIONS,
5050
REQ_GET_PROMPT,
5151
REQ_GET_VERSION,
52+
REQ_INLINE_COMPLETION_PROMPT,
53+
REQ_INLINE_COMPLETION,
5254
REQ_NOTIFY_ACCEPTED,
5355
REQ_NOTIFY_REJECTED,
5456
REQ_SIGN_IN_CONFIRM,
@@ -206,6 +208,327 @@ def run(self, plugin: CopilotPlugin, session: Session, _: sublime.Edit) -> None:
206208
plugin.request_get_completions(self.view)
207209

208210

211+
class CopilotInlineCompletionPromptCommand(CopilotTextCommand):
212+
"""Command to get inline completions with a custom prompt/message."""
213+
214+
@_provide_plugin_session()
215+
def run(self, plugin: CopilotPlugin, session: Session, _: sublime.Edit, message: str = "") -> None:
216+
# Prompt for message if not provided
217+
if not message.strip():
218+
self.view.window().show_input_panel(
219+
"Completion Prompt:",
220+
"",
221+
lambda msg: self._request_inline_completion_with_prompt(plugin, session, msg),
222+
None,
223+
None
224+
)
225+
else:
226+
self._request_inline_completion_with_prompt(plugin, session, message)
227+
228+
def _request_inline_completion_with_prompt(self, plugin: CopilotPlugin, session: Session, message: str) -> None:
229+
"""Send the inline completion request with the custom prompt."""
230+
if not message.strip():
231+
status_message("Please provide a message for the completion prompt", icon="❌")
232+
return
233+
234+
# Prepare document information
235+
if not (doc := prepare_completion_request_doc(self.view)):
236+
status_message("Failed to prepare document for completion request", icon="❌")
237+
return
238+
239+
# Get cursor position
240+
if not (sel := self.view.sel()) or len(sel) != 1:
241+
status_message("Please place cursor at a single position", icon="❌")
242+
return
243+
244+
cursor_point = sel[0].begin()
245+
row, col = self.view.rowcol(cursor_point)
246+
247+
# Prepare the request payload based on the structure from constants
248+
payload = {
249+
"textDocument": {
250+
"uri": doc["uri"]
251+
},
252+
"position": {
253+
"line": row,
254+
"character": col
255+
},
256+
"formattingOptions": {
257+
"tabSize": doc.get("tabSize", 4),
258+
"insertSpaces": doc.get("insertSpaces", True)
259+
},
260+
"context": {
261+
"triggerKind": 2
262+
},
263+
"data": {
264+
"message": message
265+
}
266+
}
267+
268+
# Send the request
269+
session.send_request(
270+
Request(REQ_INLINE_COMPLETION_PROMPT, payload),
271+
lambda response: self._on_result_inline_completion_prompt(response, message)
272+
)
273+
274+
status_message(f"Requesting completion with prompt: {message[:50]}...", icon="⏳")
275+
276+
def _on_result_inline_completion_prompt(self, response: Any, original_message: str) -> None:
277+
"""Handle the response from the inline completion prompt request."""
278+
if not response:
279+
status_message("No completion suggestions received", icon="❌")
280+
return
281+
282+
# Handle the new response format with items
283+
if isinstance(response, dict) and "items" in response:
284+
items = response["items"]
285+
if not items:
286+
status_message("No completion items received", icon="❌")
287+
return
288+
289+
# Show the completion selection dialog
290+
self.view.window().run_command("copilot_select_inline_completion", {
291+
"items": items,
292+
"original_message": original_message
293+
})
294+
else:
295+
# Fallback for other response formats
296+
status_message(f"Unexpected response format: {type(response)}", icon="❌")
297+
298+
299+
class CopilotInlineCompletionCommand(CopilotTextCommand):
300+
"""Command to get inline completions (automatic trigger)."""
301+
302+
@_provide_plugin_session()
303+
def run(self, plugin: CopilotPlugin, session: Session, _: sublime.Edit, message: str = "") -> None:
304+
# Prompt for message if not provided
305+
if not message.strip():
306+
self.view.window().show_input_panel(
307+
"Completion Message:",
308+
"",
309+
lambda msg: self._request_inline_completion(plugin, session, msg),
310+
None,
311+
None
312+
)
313+
else:
314+
self._request_inline_completion(plugin, session, message)
315+
316+
def _request_inline_completion(self, plugin: CopilotPlugin, session: Session, message: str) -> None:
317+
"""Send the inline completion request."""
318+
if not message.strip():
319+
status_message("Please provide a message for the completion", icon="❌")
320+
return
321+
322+
# Prepare document information
323+
if not (doc := prepare_completion_request_doc(self.view)):
324+
status_message("Failed to prepare document for completion request", icon="❌")
325+
return
326+
327+
# Get cursor position
328+
if not (sel := self.view.sel()) or len(sel) != 1:
329+
status_message("Please place cursor at a single position", icon="❌")
330+
return
331+
332+
cursor_point = sel[0].begin()
333+
row, col = self.view.rowcol(cursor_point)
334+
335+
# Prepare the request payload - same as prompt but with triggerKind: 1
336+
payload = {
337+
"textDocument": {
338+
"uri": doc["uri"]
339+
},
340+
"position": {
341+
"line": row,
342+
"character": col
343+
},
344+
"formattingOptions": {
345+
"tabSize": doc.get("tabSize", 4),
346+
"insertSpaces": doc.get("insertSpaces", True)
347+
},
348+
"context": {
349+
"triggerKind": 1 # Different from prompt command which uses 2
350+
},
351+
"data": {
352+
"message": message
353+
}
354+
}
355+
356+
# Send the request
357+
session.send_request(
358+
Request(REQ_INLINE_COMPLETION, payload),
359+
lambda response: self._on_result_inline_completion(response, message)
360+
)
361+
362+
status_message(f"Requesting inline completion: {message[:50]}...", icon="⏳")
363+
364+
def _on_result_inline_completion(self, response: Any, original_message: str) -> None:
365+
"""Handle the response from the inline completion request."""
366+
if not response:
367+
status_message("No completion suggestions received", icon="❌")
368+
return
369+
370+
# Handle the new response format with items
371+
if isinstance(response, dict) and "items" in response:
372+
items = response["items"]
373+
if not items:
374+
status_message("No completion items received", icon="❌")
375+
return
376+
377+
# Store the completion items for the input handler
378+
self._completion_items = items
379+
self._original_message = original_message
380+
381+
# Show the completion selection dialog
382+
self.view.window().run_command("copilot_select_inline_completion", {
383+
"items": items,
384+
"original_message": original_message
385+
})
386+
else:
387+
# Fallback for other response formats
388+
status_message(f"Unexpected response format: {type(response)}", icon="❌")
389+
390+
391+
class CopilotSelectInlineCompletionCommand(CopilotTextCommand):
392+
"""Command to select and insert an inline completion from a list."""
393+
394+
def run(self, edit: sublime.Edit, selected: int, items: list[dict[str, Any]], original_message: str, selected_item: dict[str, Any] | None = None) -> None:
395+
if selected_item:
396+
# Insert the selected completion
397+
insert_text = selected_item.get("insertText", "")
398+
if insert_text:
399+
# Get the range where to insert
400+
if "range" in selected_item:
401+
range_data = selected_item["range"]
402+
start_line = range_data["start"]["line"]
403+
start_char = range_data["start"]["character"]
404+
end_line = range_data["end"]["line"]
405+
end_char = range_data["end"]["character"]
406+
407+
# Convert to Sublime Text points
408+
start_point = self.view.text_point(start_line, start_char)
409+
end_point = self.view.text_point(end_line, end_char)
410+
411+
# Replace the range with the completion
412+
self.view.replace(edit, sublime.Region(start_point, end_point), insert_text)
413+
else:
414+
# Insert at current cursor position
415+
if sel := self.view.sel():
416+
self.view.insert(edit, sel[0].begin(), insert_text)
417+
418+
status_message(f"Inserted completion for: {original_message[:30]}...", icon="✅")
419+
420+
# Handle acceptance command if present
421+
if "command" in selected_item and selected_item["command"]:
422+
cmd = selected_item["command"]
423+
if cmd.get("command") == "github.copilot.didAcceptCompletionItem":
424+
# Send acceptance notification to Copilot
425+
args = cmd.get("arguments", [])
426+
if args:
427+
# This would typically be handled by the LSP client
428+
# For now, we'll just log it
429+
print(f"Copilot completion accepted: {args[0]}")
430+
else:
431+
status_message("Selected completion has no text", icon="❌")
432+
433+
def input(self, args: dict[str, Any]) -> sublime_plugin.CommandInputHandler | None:
434+
items = args.get("items", [])
435+
original_message = args.get("original_message", "")
436+
437+
if not items:
438+
return None
439+
440+
return CopilotInlineCompletionInputHandler(items, original_message)
441+
442+
443+
class CopilotInlineCompletionInputHandler(sublime_plugin.ListInputHandler):
444+
"""Input handler for selecting inline completions."""
445+
446+
def __init__(self, items: list[dict[str, Any]], original_message: str):
447+
self.items = items
448+
self.original_message = original_message
449+
450+
def name(self) -> str:
451+
return "selected_item"
452+
453+
def placeholder(self) -> str:
454+
return f"Select completion for: {self.original_message[:50]}..."
455+
456+
def list_items(self) -> list[sublime.ListInputItem]:
457+
import mdpopups
458+
459+
list_items = []
460+
for i, item in enumerate(self.items):
461+
insert_text = item.get("insertText", "")
462+
463+
# Create preview using mdpopups to convert markdown to HTML
464+
if insert_text:
465+
# Detect language for syntax highlighting
466+
# This is a simple heuristic - you might want to improve this
467+
language = self._detect_language(insert_text)
468+
markdown_text = f"```{language}\n{insert_text}\n```"
469+
470+
# Convert markdown to HTML using mdpopups
471+
try:
472+
html_details = mdpopups.md2html(None, markdown_text)
473+
except Exception:
474+
# Fallback to plain text if markdown conversion fails
475+
html_details = f"<pre>{insert_text}</pre>"
476+
477+
# Create a short preview for the main text
478+
preview = insert_text.strip().split('\n')[0]
479+
if len(preview) > 60:
480+
preview = preview[:57] + "..."
481+
482+
# Add line count annotation
483+
line_count = len(insert_text.split('\n'))
484+
annotation = f"{line_count} line{'s' if line_count != 1 else ''}"
485+
486+
list_items.append(sublime.ListInputItem(
487+
text=preview,
488+
value=item,
489+
details=html_details,
490+
annotation=annotation,
491+
kind=sublime.KIND_SNIPPET
492+
))
493+
else:
494+
list_items.append(sublime.ListInputItem(
495+
text=f"Completion {i + 1}",
496+
value=item,
497+
details="<em>No preview available</em>",
498+
annotation="",
499+
kind=sublime.KIND_SNIPPET
500+
))
501+
502+
return list_items
503+
504+
def _detect_language(self, text: str) -> str:
505+
"""Simple language detection based on content."""
506+
text_lower = text.lower().strip()
507+
508+
# Python
509+
if any(keyword in text_lower for keyword in ['def ', 'import ', 'from ', 'class ', 'if __name__']):
510+
return "python"
511+
512+
# JavaScript/TypeScript
513+
if any(keyword in text_lower for keyword in ['function ', 'const ', 'let ', 'var ', '=>', 'console.log']):
514+
return "javascript"
515+
516+
# HTML
517+
if text_lower.startswith('<') and '>' in text_lower:
518+
return "html"
519+
520+
# CSS
521+
if '{' in text and '}' in text and ':' in text:
522+
return "css"
523+
524+
# JSON
525+
if text.strip().startswith('{') and text.strip().endswith('}'):
526+
return "json"
527+
528+
# Default to text
529+
return "text"
530+
531+
209532
class CopilotAcceptPanelCompletionShimCommand(CopilotWindowCommand):
210533
def run(self, view_id: int, completion_index: int) -> None:
211534
if not (view := find_view_by_id(view_id)):

0 commit comments

Comments
 (0)