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 .copilotignore support to ignore files #169

Merged
merged 29 commits into from
Jun 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
e00ed91
working on ignore setting for files
TerminalFi Jun 19, 2024
646a1d2
adding CopilotIgnore class.
TerminalFi Jun 19, 2024
f57bfb2
Merge branch 'master' into impl/copilotIgnore
jfcherng Jun 19, 2024
c860149
chore: update dependencies
jfcherng Jun 20, 2024
48c4f0d
chore: update dependencies
jfcherng Jun 20, 2024
b29e461
testing decorator
TerminalFi Jun 21, 2024
306031b
unload patterns on window close
TerminalFi Jun 21, 2024
7e5993c
break out decorators
TerminalFi Jun 21, 2024
f321f06
don't ignore our python files
TerminalFi Jun 21, 2024
b38f21e
reload copilotignore command
TerminalFi Jun 21, 2024
59c9380
type for patterns
TerminalFi Jun 21, 2024
8e2e468
add view setting
TerminalFi Jun 22, 2024
0cbb74d
prep for optional `variables` for status_bar text
TerminalFi Jun 22, 2024
aeaa623
Merge branch 'master' into impl/copilotIgnore
jfcherng Jun 22, 2024
b9c8970
Merge branch 'master' into impl/copilotIgnore
jfcherng Jun 22, 2024
a5e3849
minor fix
TerminalFi Jun 23, 2024
42b1992
Merge branch 'master' into impl/copilotIgnore
jfcherng Jun 24, 2024
9928ec1
testing file listener
TerminalFi Jun 25, 2024
a583865
chore: add watchdog dep
jfcherng Jun 26, 2024
b32a458
fix: is_enabled must return a bool
jfcherng Jun 26, 2024
d0d6a16
refactor: make CI happy
jfcherng Jun 26, 2024
c2b7878
trying some ignore exempt commands
TerminalFi Jun 26, 2024
21f2979
fix completion command and ignore updates
TerminalFi Jun 26, 2024
c16d13b
fix panel accept
TerminalFi Jun 26, 2024
1d5a6b9
fix: panel completion template
jfcherng Jun 26, 2024
6ca4119
update CopilotIgnore, add status bar text
TerminalFi Jun 26, 2024
b2ccb3f
use GLOBSTAR to support `**/*`
TerminalFi Jun 26, 2024
2aa435c
clean up on init
TerminalFi Jun 26, 2024
32ae984
fix import
TerminalFi Jun 26, 2024
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
1 change: 1 addition & 0 deletions .copilotignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.env
2 changes: 1 addition & 1 deletion LSP-copilot.sublime-settings
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
"telemetry": false,
// The (Jinja2) template of the status bar text which is inside the parentheses `(...)`.
// See https://jinja.palletsprojects.com/templates/
"status_text": "{% if server_version %}v{{ server_version }}{% endif %}",
"status_text": "{% if is_copilot_ignored %}{{ is_copilot_ignored }}{% elif server_version %}v{{ server_version }}{% endif %}",
},
// ST4 configuration
"selector": "source | text | embedding"
Expand Down
5 changes: 4 additions & 1 deletion dependencies.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
{
"*": {
"*": [
"bracex",
"Jinja2",
"lsp_utils",
"markupsafe",
"more-itertools",
"sublime_lib"
"sublime_lib",
"watchdog",
"wcmatch"
]
}
}
7 changes: 6 additions & 1 deletion plugin/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
CopilotSignInWithGithubTokenCommand,
CopilotSignOutCommand,
)
from .listeners import EventListener, ViewEventListener
from .listeners import EventListener, ViewEventListener, copilot_ignore_observer
from .utils import CopilotIgnore

__all__ = (
# ST: core
Expand Down Expand Up @@ -49,9 +50,13 @@
def plugin_loaded() -> None:
"""Executed when this plugin is loaded."""
CopilotPlugin.setup()
copilot_ignore_observer.setup()


def plugin_unloaded() -> None:
"""Executed when this plugin is unloaded."""
CopilotPlugin.window_attrs.clear()
CopilotPlugin.cleanup()
CopilotIgnore.cleanup()
if copilot_ignore_observer:
copilot_ignore_observer.cleanup()
18 changes: 14 additions & 4 deletions plugin/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
REQ_SIGN_IN_WITH_GITHUB_TOKEN,
REQ_SIGN_OUT,
)
from .decorators import _must_be_active_view_not_ignored
from .types import (
CopilotPayloadFileStatus,
CopilotPayloadGetVersion,
Expand Down Expand Up @@ -109,6 +110,13 @@ def _record_telemetry(

session.send_request(Request(request, payload), lambda _: None)

@_must_be_active_view_not_ignored(failed_return=False)
@_provide_plugin_session(failed_return=False)
def is_enabled(self, plugin: CopilotPlugin, session: Session) -> bool: # type: ignore
return self._can_meet_requirement(session)


class CopilotIgnoreExemptTextCommand(CopilotTextCommand):
@_provide_plugin_session(failed_return=False)
def is_enabled(self, plugin: CopilotPlugin, session: Session) -> bool: # type: ignore
return self._can_meet_requirement(session)
Expand All @@ -121,7 +129,7 @@ def is_enabled(self) -> bool:
return self._can_meet_requirement(session)


class CopilotGetVersionCommand(CopilotTextCommand):
class CopilotGetVersionCommand(CopilotIgnoreExemptTextCommand):
requirement = REQUIRE_NOTHING

@_provide_plugin_session()
Expand All @@ -142,6 +150,8 @@ class CopilotAcceptPanelCompletionShimCommand(CopilotWindowCommand):
def run(self, view_id: int, completion_index: int) -> None:
if not (view := find_view_by_id(view_id)):
return
# Focus the view so that the command runs
self.window.focus_view(view)
view.run_command("copilot_accept_panel_completion", {"completion_index": completion_index})


Expand Down Expand Up @@ -278,7 +288,7 @@ def _on_result_check_file_status(self, payload: CopilotPayloadFileStatus) -> Non
status_message("File is {} in session", payload["status"])


class CopilotSignInCommand(CopilotTextCommand):
class CopilotSignInCommand(CopilotIgnoreExemptTextCommand):
requirement = REQUIRE_NOT_SIGN_IN

@_provide_plugin_session()
Expand Down Expand Up @@ -317,7 +327,7 @@ def _on_result_sign_in_confirm(self, payload: CopilotPayloadSignInConfirm) -> No
self.view.run_command("copilot_check_status")


class CopilotSignInWithGithubTokenCommand(CopilotTextCommand):
class CopilotSignInWithGithubTokenCommand(CopilotIgnoreExemptTextCommand):
requirement = REQUIRE_NOT_SIGN_IN

@_provide_plugin_session()
Expand Down Expand Up @@ -366,7 +376,7 @@ def _on_result_sign_in_confirm(self, payload: CopilotPayloadSignInConfirm) -> No
self.view.run_command("copilot_check_status")


class CopilotSignOutCommand(CopilotTextCommand):
class CopilotSignOutCommand(CopilotIgnoreExemptTextCommand):
requirement = REQUIRE_SIGN_IN

@_provide_plugin_session()
Expand Down
1 change: 1 addition & 0 deletions plugin/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
PACKAGE_NAME = __package__.partition(".")[0]

COPILOT_VIEW_SETTINGS_PREFIX = "copilot.completion"
COPILOT_WINDOW_SETTINGS_PREFIX = "copilot"

# ---------------- #
# Copilot requests #
Expand Down
21 changes: 21 additions & 0 deletions plugin/decorators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from functools import wraps
from typing import Any, Callable, cast

from .types import T_Callable
from .utils import (
CopilotIgnore,
is_active_view,
)


def _must_be_active_view_not_ignored(*, failed_return: Any = None) -> Callable[[T_Callable], T_Callable]:
def decorator(func: T_Callable) -> T_Callable:
@wraps(func)
def wrapped(self: Any, *arg, **kwargs) -> Any:
if is_active_view(self.view) and not CopilotIgnore(self.view.window()).trigger(self.view):
return func(self, *arg, **kwargs)
return failed_return

return cast(T_Callable, wrapped)

return decorator
126 changes: 106 additions & 20 deletions plugin/listeners.py
Original file line number Diff line number Diff line change
@@ -1,33 +1,28 @@
from __future__ import annotations

import re
from collections.abc import Callable
from functools import wraps
from typing import Any, cast
from typing import Any

import sublime
import sublime_plugin
from watchdog.events import FileSystemEventHandler
from watchdog.observers import Observer

from .client import CopilotPlugin
from .types import T_Callable
from .decorators import _must_be_active_view_not_ignored
from .ui import ViewCompletionManager, ViewPanelCompletionManager
from .utils import get_copilot_view_setting, get_session_setting, is_active_view, set_copilot_view_setting


def _must_be_active_view(*, failed_return: Any = None) -> Callable[[T_Callable], T_Callable]:
def decorator(func: T_Callable) -> T_Callable:
@wraps(func)
def wrapped(self: Any, *arg, **kwargs) -> Any:
if is_active_view(self.view):
return func(self, *arg, **kwargs)
return failed_return

return cast(T_Callable, wrapped)

return decorator
from .utils import (
CopilotIgnore,
get_copilot_view_setting,
get_session_setting,
set_copilot_view_setting,
)


class ViewEventListener(sublime_plugin.ViewEventListener):
def __init__(self, view: sublime.View) -> None:
super().__init__(view)

@classmethod
def applies_to_primary_view_only(cls) -> bool:
# To fix "https://github.com/TerminalFi/LSP-copilot/issues/102",
Expand All @@ -51,7 +46,7 @@ def _is_saving(self) -> bool:
def _is_saving(self, value: bool) -> None:
set_copilot_view_setting(self.view, "_is_saving", value)

@_must_be_active_view()
@_must_be_active_view_not_ignored()
def on_modified_async(self) -> None:
self._is_modified = True

Expand All @@ -65,6 +60,20 @@ def on_modified_async(self) -> None:
if not self._is_saving and get_session_setting(session, "auto_ask_completions") and not vcm.is_waiting:
plugin.request_get_completions(self.view)

def on_activated_async(self) -> None:
if (
(window := self.view.window())
and (plugin := CopilotPlugin.from_view(self.view))
and copilot_ignore_observer
):
copilot_ignore_observer.add_folders(window.folders())
CopilotIgnore(window).load_patterns()
CopilotIgnore(window).trigger(self.view)
if get_copilot_view_setting(self.view, "is_copilot_ignored", False):
plugin.update_status_bar_text({"is_copilot_ignored": "ignored"})
else:
plugin.update_status_bar_text()

def on_deactivated_async(self) -> None:
ViewCompletionManager(self.view).hide()

Expand Down Expand Up @@ -124,7 +133,7 @@ def on_post_text_command(self, command_name: str, args: dict[str, Any] | None) -
def on_post_save_async(self) -> None:
self._is_saving = False

@_must_be_active_view()
@_must_be_active_view_not_ignored()
def on_selection_modified_async(self) -> None:
if not self._is_modified:
ViewCompletionManager(self.view).handle_selection_change()
Expand All @@ -149,3 +158,80 @@ def on_window_command(
return "noop", None

return None

def on_new_window(self, window: sublime.Window):
if not copilot_ignore_observer:
return
copilot_ignore_observer.add_folders(window.folders())

def on_pre_close_window(self, window: sublime.Window):
if not copilot_ignore_observer:
return
copilot_ignore_observer.remove_folders(window.folders())


class CopilotIgnoreHandler(FileSystemEventHandler):
def __init__(self):
self.filename = ".copilotignore"

def on_modified(self, event):
if not event.is_directory and event.src_path.endswith(self.filename):
self.update_window_patterns(event.src_path)

def on_created(self, event):
if not event.is_directory and event.src_path.endswith(self.filename):
self.update_window_patterns(event.src_path)

def update_window_patterns(self, path: str):
windows = sublime.windows()
for window in windows:
if not self._best_matched_folder(path, window.folders()):
continue
# Update patterns for specific window and folder
CopilotIgnore(window).load_patterns()
return

def _best_matched_folder(self, path: str, folders: list[str]) -> str | None:
matching_folder = None
for folder in folders:
if path.startswith(folder) and (matching_folder is None or len(folder) > len(matching_folder)):
matching_folder = folder
return matching_folder


class CopilotIgnoreObserver:
def __init__(self, folders: list[str] = []):
self.observer = Observer()
self._event_handler = CopilotIgnoreHandler()
self._folders: list[str] = folders
self._observers: dict[str, Any] = {}

def setup(self):
self.add_folders(self._folders)
self.observer.start()

def cleanup(self):
self.observer.stop()
self.observer.join()

def add_folders(self, folders: list[str]):
for folder in folders:
self.add_folder(folder)

def add_folder(self, folder):
if folder not in self._folders:
self._folders.append(folder)
observer = self.observer.schedule(self._event_handler, folder, recursive=False)
self._observers[folder] = observer

def remove_folders(self, folders: list[str]):
for folder in folders:
self.remove_folder(folder)

def remove_folder(self, folder):
if folder in self._folders:
self._folders.remove(folder)
self.observer.unschedule(self._observers[folder])


copilot_ignore_observer = CopilotIgnoreObserver()
6 changes: 3 additions & 3 deletions plugin/templates/panel_completion.md.jinja
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,9 @@
<a class="accept" title="Accept Completion" href='{{ section.accept_url }}'><i>✓</i> Accept</a>
</div>

``````{{ section.lang }}
{{ section.code }}
``````
``````{{ section.lang }}
{{ section.code }}
``````

{% endfor %}

Expand Down
4 changes: 2 additions & 2 deletions plugin/ui/panel_completion.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ def close(self) -> None:
sheet.close()
self.completion_manager.is_visible = False
if self.completion_manager.original_layout:
window.set_layout(self.completion_manager.original_layout)
window.set_layout(self.completion_manager.original_layout) # type: ignore
self.completion_manager.original_layout = None

window.focus_view(self.view)
Expand Down Expand Up @@ -252,7 +252,7 @@ def _open_in_group(self, window: sublime.Window, group_id: int) -> None:
self.completion_manager.sheet_id = sheet.id()

def _open_in_side_by_side(self, window: sublime.Window) -> None:
self.completion_manager.original_layout = window.layout()
self.completion_manager.original_layout = window.layout() # type: ignore
window.set_layout({
"cols": [0.0, 0.5, 1.0],
"rows": [0.0, 1.0],
Expand Down
Loading