summaryrefslogtreecommitdiff
path: root/server/continuedev/models
diff options
context:
space:
mode:
authorNate Sesti <33237525+sestinj@users.noreply.github.com>2023-10-09 18:37:27 -0700
committerGitHub <noreply@github.com>2023-10-09 18:37:27 -0700
commitf09150617ed2454f3074bcf93f53aae5ae637d40 (patch)
tree5cfe614a64d921dfe58b049f426d67a8b832c71f /server/continuedev/models
parent985304a213f620cdff3f8f65f74ed7e3b79be29d (diff)
downloadsncontinue-f09150617ed2454f3074bcf93f53aae5ae637d40.tar.gz
sncontinue-f09150617ed2454f3074bcf93f53aae5ae637d40.tar.bz2
sncontinue-f09150617ed2454f3074bcf93f53aae5ae637d40.zip
Preview (#541)
* Strong typing (#533) * refactor: :recycle: get rid of continuedev.src.continuedev structure * refactor: :recycle: switching back to server folder * feat: :sparkles: make config.py imports shorter * feat: :bookmark: publish as pre-release vscode extension * refactor: :recycle: refactor and add more completion params to ui * build: :building_construction: download from preview S3 * fix: :bug: fix paths * fix: :green_heart: package:pre-release * ci: :green_heart: more time for tests * fix: :green_heart: fix build scripts * fix: :bug: fix import in run.py * fix: :bookmark: update version to try again * ci: 💚 Update package.json version [skip ci] * refactor: :fire: don't check for old extensions version * fix: :bug: small bug fixes * fix: :bug: fix config.py import paths * ci: 💚 Update package.json version [skip ci] * ci: :green_heart: platform-specific builds test #1 * feat: :green_heart: ship with binary * fix: :green_heart: fix copy statement to include.exe for windows * fix: :green_heart: cd extension before packaging * chore: :loud_sound: count tokens generated * fix: :green_heart: remove npm_config_arch * fix: :green_heart: publish as pre-release! * chore: :bookmark: update version * perf: :green_heart: hardcode distro paths * fix: :bug: fix yaml syntax error * chore: :bookmark: update version * fix: :green_heart: update permissions and version * feat: :bug: kill old server if needed * feat: :lipstick: update marketplace icon for pre-release * ci: 💚 Update package.json version [skip ci] * feat: :sparkles: auto-reload for config.py * feat: :wrench: update default config.py imports * feat: :sparkles: codelens in config.py * feat: :sparkles: select model param count from UI * ci: 💚 Update package.json version [skip ci] * feat: :sparkles: more model options, ollama error handling * perf: :zap: don't show server loading immediately * fix: :bug: fixing small UI details * ci: 💚 Update package.json version [skip ci] * feat: :rocket: headers param on LLM class * fix: :bug: fix headers for openai.;y * feat: :sparkles: highlight code on cmd+shift+L * ci: 💚 Update package.json version [skip ci] * feat: :lipstick: sticky top bar in gui.tsx * fix: :loud_sound: websocket logging and horizontal scrollbar * ci: 💚 Update package.json version [skip ci] * feat: :sparkles: allow AzureOpenAI Service through GGML * ci: 💚 Update package.json version [skip ci] * fix: :bug: fix automigration * ci: 💚 Update package.json version [skip ci] * ci: :green_heart: upload binaries in ci, download apple silicon * chore: :fire: remove notes * fix: :green_heart: use curl to download binary * fix: :green_heart: set permissions on apple silicon binary * fix: :green_heart: testing * fix: :green_heart: cleanup file * fix: :green_heart: fix preview.yaml * fix: :green_heart: only upload once per binary * fix: :green_heart: install rosetta * ci: :green_heart: download binary after tests * ci: 💚 Update package.json version [skip ci] * ci: :green_heart: prepare ci for merge to main --------- Co-authored-by: GitHub Action <action@github.com>
Diffstat (limited to 'server/continuedev/models')
-rw-r--r--server/continuedev/models/__init__.py0
-rw-r--r--server/continuedev/models/filesystem.py398
-rw-r--r--server/continuedev/models/filesystem_edit.py164
-rw-r--r--server/continuedev/models/generate_json_schema.py54
-rw-r--r--server/continuedev/models/main.py229
-rw-r--r--server/continuedev/models/reference/generate.py144
6 files changed, 989 insertions, 0 deletions
diff --git a/server/continuedev/models/__init__.py b/server/continuedev/models/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/server/continuedev/models/__init__.py
diff --git a/server/continuedev/models/filesystem.py b/server/continuedev/models/filesystem.py
new file mode 100644
index 00000000..27244c4b
--- /dev/null
+++ b/server/continuedev/models/filesystem.py
@@ -0,0 +1,398 @@
+import os
+from abc import abstractmethod
+from typing import Dict, List, Tuple
+
+from pydantic import BaseModel
+
+from ..models.main import AbstractModel, Position, Range
+from .filesystem_edit import (
+ AddDirectory,
+ AddFile,
+ DeleteDirectory,
+ DeleteFile,
+ EditDiff,
+ FileEdit,
+ FileSystemEdit,
+ RenameDirectory,
+ RenameFile,
+ SequentialFileSystemEdit,
+)
+
+
+class RangeInFile(BaseModel):
+ filepath: str
+ range: Range
+
+ def __hash__(self):
+ return hash((self.filepath, self.range))
+
+ @staticmethod
+ def from_entire_file(filepath: str, content: str) -> "RangeInFile":
+ range = Range.from_entire_file(content)
+ return RangeInFile(filepath=filepath, range=range)
+
+ def translated(self, lines: int):
+ return RangeInFile(filepath=self.filepath, range=self.range.translated(lines))
+
+
+class RangeInFileWithContents(RangeInFile):
+ """A range in a file with the contents of the range."""
+
+ contents: str
+
+ def __hash__(self):
+ return hash((self.filepath, self.range, self.contents))
+
+ def union(self, other: "RangeInFileWithContents") -> "RangeInFileWithContents":
+ assert self.filepath == other.filepath
+ # Use a placeholder variable for self and swap it with other if other comes before self
+ first = self
+ second = other
+ if other.range.start < self.range.start:
+ first = other
+ second = self
+
+ assert first.filepath == second.filepath
+
+ # Calculate union of contents
+ num_overlapping_lines = first.range.end.line - second.range.start.line + 1
+ union_lines = (
+ first.contents.splitlines()[:-num_overlapping_lines]
+ + second.contents.splitlines()
+ )
+
+ return RangeInFileWithContents(
+ filepath=first.filepath,
+ range=first.range.union(second.range),
+ contents="\n".join(union_lines),
+ )
+
+ @staticmethod
+ def from_entire_file(filepath: str, content: str) -> "RangeInFileWithContents":
+ lines = content.splitlines()
+ if not lines:
+ return RangeInFileWithContents(
+ filepath=filepath, range=Range.from_shorthand(0, 0, 0, 0), contents=""
+ )
+ return RangeInFileWithContents(
+ filepath=filepath,
+ range=Range.from_shorthand(0, 0, len(lines) - 1, len(lines[-1]) - 1),
+ contents=content,
+ )
+
+ @staticmethod
+ def from_range_in_file(rif: RangeInFile, content: str) -> "RangeInFileWithContents":
+ return RangeInFileWithContents(
+ filepath=rif.filepath, range=rif.range, contents=content
+ )
+
+
+class FileSystem(AbstractModel):
+ """An abstract filesystem that can read/write from a set of files."""
+
+ @abstractmethod
+ def read(self, path) -> str:
+ raise NotImplementedError
+
+ @abstractmethod
+ def readlines(self, path) -> List[str]:
+ raise NotImplementedError
+
+ @abstractmethod
+ def write(self, path, content):
+ raise NotImplementedError
+
+ @abstractmethod
+ def exists(self, path) -> bool:
+ raise NotImplementedError
+
+ @abstractmethod
+ def read_range_in_file(self, r: RangeInFile) -> str:
+ raise NotImplementedError
+
+ @abstractmethod
+ def rename_file(self, filepath: str, new_filepath: str):
+ raise NotImplementedError
+
+ @abstractmethod
+ def rename_directory(self, path: str, new_path: str):
+ raise NotImplementedError
+
+ @abstractmethod
+ def delete_file(self, filepath: str):
+ raise NotImplementedError
+
+ @abstractmethod
+ def delete_directory(self, path: str):
+ raise NotImplementedError
+
+ @abstractmethod
+ def add_directory(self, path: str):
+ raise NotImplementedError
+
+ @abstractmethod
+ def apply_file_edit(self, edit: FileEdit) -> EditDiff:
+ raise NotImplementedError
+
+ @abstractmethod
+ def apply_edit(self, edit: FileSystemEdit) -> EditDiff:
+ """Apply edit to filesystem, calculate the reverse edit, and return and EditDiff"""
+ raise NotImplementedError
+
+ @abstractmethod
+ def list_directory_contents(self, path: str, recursive: bool = False) -> List[str]:
+ """List the contents of a directory"""
+ raise NotImplementedError
+
+ @classmethod
+ def read_range_in_str(self, s: str, r: Range) -> str:
+ lines = s.split("\n")[r.start.line : r.end.line + 1]
+ if len(lines) == 0:
+ return ""
+
+ lines[0] = lines[0][r.start.character :]
+ lines[-1] = lines[-1][: r.end.character + 1]
+ return "\n".join(lines)
+
+ @classmethod
+ def apply_edit_to_str(cls, s: str, edit: FileEdit) -> Tuple[str, EditDiff]:
+ original = cls.read_range_in_str(s, edit.range)
+
+ # Split lines and deal with some edge cases (could obviously be nicer)
+ lines = s.splitlines()
+ if s.startswith("\n"):
+ lines.insert(0, "")
+ if s.endswith("\n"):
+ lines.append("")
+
+ if len(lines) == 0:
+ lines = [""]
+
+ end = Position(line=edit.range.end.line, character=edit.range.end.character)
+ if edit.range.end.line == len(lines) and edit.range.end.character == 0:
+ end = Position(
+ line=edit.range.end.line - 1,
+ character=len(lines[min(len(lines) - 1, edit.range.end.line - 1)]),
+ )
+
+ before_lines = lines[: edit.range.start.line]
+ after_lines = lines[end.line + 1 :]
+ between_str = (
+ lines[min(len(lines) - 1, edit.range.start.line)][
+ : edit.range.start.character
+ ]
+ + edit.replacement
+ + lines[min(len(lines) - 1, end.line)][end.character + 1 :]
+ )
+
+ new_range = Range(
+ start=edit.range.start,
+ end=Position(
+ line=edit.range.start.line + len(edit.replacement.splitlines()) - 1,
+ character=edit.range.start.character
+ + len(edit.replacement.splitlines()[-1])
+ if edit.replacement != ""
+ else 0,
+ ),
+ )
+
+ lines = before_lines + between_str.splitlines() + after_lines
+ return "\n".join(lines), EditDiff(
+ forward=edit,
+ backward=FileEdit(
+ filepath=edit.filepath, range=new_range, replacement=original
+ ),
+ )
+
+ def reverse_edit_on_str(self, s: str, diff: EditDiff) -> str:
+ lines = s.splitlines()
+
+ replacement_lines = diff.replacement.splitlines()
+ replacement_d_lines = len(replacement_lines)
+ replacement_d_chars = len(replacement_lines[-1])
+ replacement_range = Range(
+ start=diff.edit.range.start,
+ end=Position(
+ line=diff.edit.range.start + replacement_d_lines,
+ character=diff.edit.range.start.character + replacement_d_chars,
+ ),
+ )
+
+ before_lines = lines[: replacement_range.start.line]
+ after_lines = lines[replacement_range.end.line + 1 :]
+ between_str = (
+ lines[replacement_range.start.line][: replacement_range.start.character]
+ + diff.original
+ + lines[replacement_range.end.line][replacement_range.end.character + 1 :]
+ )
+
+ lines = before_lines + between_str.splitlines() + after_lines
+ return "\n".join(lines)
+
+ def apply_edit(self, edit: FileSystemEdit) -> EditDiff:
+ backward = None
+ if isinstance(edit, FileEdit):
+ diff = self.apply_file_edit(edit)
+ backward = diff.backward
+ elif isinstance(edit, AddFile):
+ self.write(edit.filepath, edit.content)
+ backward = DeleteFile(edit.filepath)
+ elif isinstance(edit, DeleteFile):
+ contents = self.read(edit.filepath)
+ backward = AddFile(edit.filepath, contents)
+ self.delete_file(edit.filepath)
+ elif isinstance(edit, RenameFile):
+ self.rename_file(edit.filepath, edit.new_filepath)
+ backward = RenameFile(
+ filepath=edit.new_filepath, new_filepath=edit.filepath
+ )
+ elif isinstance(edit, AddDirectory):
+ self.add_directory(edit.path)
+ backward = DeleteDirectory(edit.path)
+ elif isinstance(edit, DeleteDirectory):
+ # This isn't atomic!
+ backward_edits = []
+ for root, dirs, files in os.walk(edit.path, topdown=False):
+ for f in files:
+ path = os.path.join(root, f)
+ backward_edits.append(self.apply_edit(DeleteFile(path)))
+ for d in dirs:
+ path = os.path.join(root, d)
+ backward_edits.append(self.apply_edit(DeleteDirectory(path)))
+
+ backward_edits.append(self.apply_edit(DeleteDirectory(edit.path)))
+ backward_edits.reverse()
+ backward = SequentialFileSystemEdit(edits=backward_edits)
+ elif isinstance(edit, RenameDirectory):
+ self.rename_directory(edit.path, edit.new_path)
+ backward = RenameDirectory(path=edit.new_path, new_path=edit.path)
+ elif isinstance(edit, FileSystemEdit):
+ backward_edits = []
+ for edit in edit.next_edit():
+ backward_edits.append(self.apply_edit(edit))
+ backward_edits.reverse()
+ backward = SequentialFileSystemEdit(edits=backward_edits)
+ else:
+ raise TypeError("Unknown FileSystemEdit type: " + str(type(edit)))
+
+ return EditDiff(forward=edit, backward=backward)
+
+
+class RealFileSystem(FileSystem):
+ """A filesystem that reads/writes from the actual filesystem."""
+
+ def read(self, path) -> str:
+ with open(path, "r") as f:
+ return f.read()
+
+ def readlines(self, path) -> List[str]:
+ with open(path, "r") as f:
+ return f.readlines()
+
+ def write(self, path, content):
+ with open(path, "w") as f:
+ f.write(content)
+
+ def exists(self, path) -> bool:
+ return os.path.exists(path)
+
+ def read_range_in_file(self, r: RangeInFile) -> str:
+ return FileSystem.read_range_in_str(self.read(r.filepath), r.range)
+
+ def rename_file(self, filepath: str, new_filepath: str):
+ os.rename(filepath, new_filepath)
+
+ def rename_directory(self, path: str, new_path: str):
+ os.rename(path, new_path)
+
+ def delete_file(self, filepath: str):
+ os.remove(filepath)
+
+ def delete_directory(self, path: str):
+ raise NotImplementedError
+
+ def add_directory(self, path: str):
+ os.makedirs(path)
+
+ def apply_file_edit(self, edit: FileEdit) -> EditDiff:
+ old_content = self.read(edit.filepath)
+ new_content, diff = FileSystem.apply_edit_to_str(old_content, edit)
+ self.write(edit.filepath, new_content)
+ return diff
+
+ def list_directory_contents(self, path: str, recursive: bool = False) -> List[str]:
+ """List the contents of a directory"""
+ if recursive:
+ # Walk
+ paths = []
+ for root, dirs, files in os.walk(path):
+ for f in files:
+ paths.append(os.path.join(root, f))
+
+ return paths
+ return list(map(lambda x: os.path.join(path, x), os.listdir(path)))
+
+
+class VirtualFileSystem(FileSystem):
+ """A simulated filesystem from a mapping of filepath to file contents."""
+
+ files: Dict[str, str]
+
+ def __init__(self, files: Dict[str, str]):
+ self.files = files
+
+ def read(self, path) -> str:
+ return self.files[path]
+
+ def readlines(self, path) -> List[str]:
+ return self.files[path].splitlines()
+
+ def write(self, path, content):
+ self.files[path] = content
+
+ def exists(self, path) -> bool:
+ return path in self.files
+
+ def read_range_in_file(self, r: RangeInFile) -> str:
+ return FileSystem.read_range_in_str(self.read(r.filepath), r.range)
+
+ def rename_file(self, filepath: str, new_filepath: str):
+ self.files[new_filepath] = self.files[filepath]
+ del self.files[filepath]
+
+ def rename_directory(self, path: str, new_path: str):
+ for filepath in self.files:
+ if filepath.startswith(path):
+ new_filepath = new_path + filepath[len(path) :]
+ self.files[new_filepath] = self.files[filepath]
+ del self.files[filepath]
+
+ def delete_file(self, filepath: str):
+ del self.files[filepath]
+
+ def delete_directory(self, path: str):
+ raise NotImplementedError
+
+ def add_directory(self, path: str):
+ # For reasons as seen here and in delete_directory, a Dict[str, str] might not be the best representation. Could just preprocess to something better upon __init__
+ pass
+
+ def apply_file_edit(self, edit: FileEdit) -> EditDiff:
+ old_content = self.read(edit.filepath)
+ new_content, original = FileSystem.apply_edit_to_str(old_content, edit)
+ self.write(edit.filepath, new_content)
+ return EditDiff(edit=edit, original=original)
+
+ def list_directory_contents(self, path: str, recursive: bool = False) -> List[str]:
+ """List the contents of a directory"""
+ if recursive:
+ for filepath in self.files:
+ if filepath.startswith(path):
+ yield filepath
+
+ for filepath in self.files:
+ if filepath.startswith(path) and "/" not in filepath[len(path) :]:
+ yield filepath
+
+
+# TODO: Uniform errors thrown by any FileSystem subclass.
diff --git a/server/continuedev/models/filesystem_edit.py b/server/continuedev/models/filesystem_edit.py
new file mode 100644
index 00000000..9316ff46
--- /dev/null
+++ b/server/continuedev/models/filesystem_edit.py
@@ -0,0 +1,164 @@
+import os
+from abc import abstractmethod
+from typing import Generator, List
+
+from pydantic import BaseModel
+
+from ..libs.util.map_path import map_path
+from .main import Position, Range
+
+
+class FileSystemEdit(BaseModel):
+ @abstractmethod
+ def next_edit(self) -> Generator["FileSystemEdit", None, None]:
+ raise NotImplementedError
+
+ @abstractmethod
+ def with_mapped_paths(self, orig_root: str, copy_root: str) -> "FileSystemEdit":
+ raise NotImplementedError
+
+
+class AtomicFileSystemEdit(FileSystemEdit):
+ def next_edit(self) -> Generator["FileSystemEdit", None, None]:
+ yield self
+
+
+class FileEdit(AtomicFileSystemEdit):
+ filepath: str
+ range: Range
+ replacement: str
+
+ def with_mapped_paths(self, orig_root: str, copy_root: str) -> "FileSystemEdit":
+ return FileEdit(
+ map_path(self.filepath, orig_root, copy_root), self.range, self.replacement
+ )
+
+ @staticmethod
+ def from_deletion(filepath: str, range: Range) -> "FileEdit":
+ return FileEdit(filepath=filepath, range=range, replacement="")
+
+ @staticmethod
+ def from_insertion(filepath: str, position: Position, content: str) -> "FileEdit":
+ return FileEdit(
+ filepath=filepath,
+ range=Range.from_shorthand(
+ position.line, position.character, position.line, position.character
+ ),
+ replacement=content,
+ )
+
+ @staticmethod
+ def from_append(
+ filepath: str, previous_content: str, appended_content: str
+ ) -> "FileEdit":
+ return FileEdit(
+ filepath=filepath,
+ range=Range.from_position(Position.from_end_of_file(previous_content)),
+ replacement=appended_content,
+ )
+
+
+class FileEditWithFullContents(BaseModel):
+ fileEdit: FileEdit
+ fileContents: str
+
+
+class AddFile(AtomicFileSystemEdit):
+ filepath: str
+ content: str
+
+ def with_mapped_paths(self, orig_root: str, copy_root: str) -> "FileSystemEdit":
+ return AddFile(
+ self, map_path(self.filepath, orig_root, copy_root), self.content
+ )
+
+
+class DeleteFile(AtomicFileSystemEdit):
+ filepath: str
+
+ def with_mapped_paths(self, orig_root: str, copy_root: str) -> "FileSystemEdit":
+ return DeleteFile(map_path(self.filepath, orig_root, copy_root))
+
+
+class RenameFile(AtomicFileSystemEdit):
+ filepath: str
+ new_filepath: str
+
+ def with_mapped_paths(self, orig_root: str, copy_root: str) -> "FileSystemEdit":
+ return RenameFile(
+ map_path(self.filepath, orig_root, copy_root),
+ map_path(self.new_filepath, orig_root, copy_root),
+ )
+
+
+class AddDirectory(AtomicFileSystemEdit):
+ path: str
+
+ def with_mapped_paths(self, orig_root: str, copy_root: str) -> "FileSystemEdit":
+ return AddDirectory(map_path(self.path, orig_root, copy_root))
+
+
+class DeleteDirectory(AtomicFileSystemEdit):
+ path: str
+
+ def with_mapped_paths(self, orig_root: str, copy_root: str) -> "FileSystemEdit":
+ return DeleteDirectory(map_path(self.path, orig_root, copy_root))
+
+
+class RenameDirectory(AtomicFileSystemEdit):
+ path: str
+ new_path: str
+
+ def with_mapped_paths(self, orig_root: str, copy_root: str) -> "FileSystemEdit":
+ return RenameDirectory(
+ map_path(self.filepath, orig_root, copy_root),
+ map_path(self.new_path, orig_root, copy_root),
+ )
+
+
+class DeleteDirectoryRecursive(FileSystemEdit):
+ path: str
+
+ def with_mapped_paths(self, orig_root: str, copy_root: str) -> "FileSystemEdit":
+ return DeleteDirectoryRecursive(map_path(self.path, orig_root, copy_root))
+
+ def next_edit(self) -> Generator[FileSystemEdit, None, None]:
+ yield DeleteDirectory(path=self.path)
+ for child in os.listdir(self.path):
+ child_path = os.path.join(self.path, child)
+ if os.path.isdir(child_path):
+ yield DeleteDirectoryRecursive(path=child_path)
+ else:
+ yield DeleteFile(filepath=child_path)
+
+
+class SequentialFileSystemEdit(FileSystemEdit):
+ edits: List[FileSystemEdit]
+
+ def with_mapped_paths(self, orig_root: str, copy_root: str) -> "FileSystemEdit":
+ return SequentialFileSystemEdit(
+ [edit.with_mapped_paths(orig_root, copy_root) for edit in self.edits]
+ )
+
+ def next_edit(self) -> Generator["FileSystemEdit", None, None]:
+ for edit in self.edits:
+ yield from edit.next_edit()
+
+
+class EditDiff(BaseModel):
+ """A reversible edit that can be applied to a file."""
+
+ forward: FileSystemEdit
+ backward: FileSystemEdit
+
+ @classmethod
+ def from_sequence(cls, diffs: List["EditDiff"]) -> "EditDiff":
+ forwards = []
+ backwards = []
+ for diff in diffs:
+ forwards.append(diff.forward)
+ backwards.insert(0, diff.backward)
+ return cls(
+ forward=SequentialFileSystemEdit(edits=forwards),
+ backward=SequentialFileSystemEdit(edits=backwards),
+ )
diff --git a/server/continuedev/models/generate_json_schema.py b/server/continuedev/models/generate_json_schema.py
new file mode 100644
index 00000000..88a1db68
--- /dev/null
+++ b/server/continuedev/models/generate_json_schema.py
@@ -0,0 +1,54 @@
+import os
+
+from pydantic import schema_json_of
+
+from ..core.config import ContinueConfig
+from ..core.context import ContextItem
+from ..core.main import FullState, History, HistoryNode, SessionInfo
+from ..core.models import Models
+from ..libs.llm.base import LLM
+from .filesystem import FileEdit, RangeInFile
+from .filesystem_edit import FileEditWithFullContents
+from .main import Position, Range, Traceback, TracebackFrame
+
+MODELS_TO_GENERATE = (
+ [Position, Range, Traceback, TracebackFrame]
+ + [RangeInFile, FileEdit]
+ + [FileEditWithFullContents]
+ + [History, HistoryNode, FullState, SessionInfo]
+ + [ContinueConfig]
+ + [ContextItem]
+ + [Models]
+ + [LLM]
+)
+
+RENAMES = {"ExampleClass": "RenamedName"}
+
+SCHEMA_DIR = "../schema/json"
+
+
+def clear_schemas():
+ for filename in os.listdir(SCHEMA_DIR):
+ if filename.endswith(".json"):
+ os.remove(os.path.join(SCHEMA_DIR, filename))
+
+
+def main():
+ clear_schemas()
+ for model in MODELS_TO_GENERATE:
+ title = RENAMES.get(model.__name__, model.__name__)
+ try:
+ json = schema_json_of(model, indent=2, title=title)
+ except Exception as e:
+ import traceback
+
+ print(f"Failed to generate json schema for {title}: {e}")
+ traceback.print_exc()
+ continue # pun intended
+
+ with open(f"{SCHEMA_DIR}/{title}.json", "w") as f:
+ f.write(json)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/server/continuedev/models/main.py b/server/continuedev/models/main.py
new file mode 100644
index 00000000..5519d718
--- /dev/null
+++ b/server/continuedev/models/main.py
@@ -0,0 +1,229 @@
+from abc import ABC
+from functools import total_ordering
+from typing import List, Tuple, Union
+
+from pydantic import BaseModel, root_validator
+
+
+class ContinueBaseModel(BaseModel):
+ class Config:
+ underscore_attrs_are_private = True
+
+
+@total_ordering
+class Position(BaseModel):
+ line: int
+ character: int
+
+ def __hash__(self):
+ return hash((self.line, self.character))
+
+ def __eq__(self, other: "Position") -> bool:
+ return self.line == other.line and self.character == other.character
+
+ def __lt__(self, other: "Position") -> bool:
+ if self.line < other.line:
+ return True
+ elif self.line == other.line:
+ return self.character < other.character
+ else:
+ return False
+
+ @staticmethod
+ def from_index(string: str, index: int) -> "Position":
+ """Convert index in string to line and character"""
+ line = string.count("\n", 0, index)
+ if line == 0:
+ character = index
+ else:
+ character = index - string.rindex("\n", 0, index) - 1
+
+ return Position(line=line, character=character)
+
+ @staticmethod
+ def from_end_of_file(contents: str) -> "Position":
+ return Position.from_index(contents, len(contents))
+
+ def to_index(self, string: str) -> int:
+ """Convert line and character to index in string"""
+ lines = string.splitlines()
+ return sum(map(len, lines[: self.line])) + self.character
+
+
+class PositionInFile(BaseModel):
+ position: Position
+ filepath: str
+
+
+class Range(BaseModel):
+ """A range in a file. 0-indexed."""
+
+ start: Position
+ end: Position
+
+ def __lt__(self, other: "Range") -> bool:
+ return self.start < other.start or (
+ self.start == other.start and self.end < other.end
+ )
+
+ def __eq__(self, other: "Range") -> bool:
+ return self.start == other.start and self.end == other.end
+
+ def __hash__(self):
+ return hash((self.start, self.end))
+
+ def union(self, other: "Range") -> "Range":
+ return Range(
+ start=min(self.start, other.start),
+ end=max(self.end, other.end),
+ )
+
+ def is_empty(self) -> bool:
+ return self.start == self.end
+
+ def indices_in_string(self, string: str) -> Tuple[int, int]:
+ """Get the start and end indices of this range in the string"""
+ lines = string.splitlines()
+ if len(lines) == 0:
+ return (0, 0)
+
+ start_index = (
+ sum([len(line) + 1 for line in lines[: self.start.line]])
+ + self.start.character
+ )
+ end_index = (
+ sum([len(line) + 1 for line in lines[: self.end.line]]) + self.end.character
+ )
+ return (start_index, end_index)
+
+ def overlaps_with(self, other: "Range") -> bool:
+ return not (self.end < other.start or self.start > other.end)
+
+ def to_full_lines(self) -> "Range":
+ return Range(
+ start=Position(line=self.start.line, character=0),
+ end=Position(line=self.end.line + 1, character=0),
+ )
+
+ def translated(self, lines: int):
+ return Range(
+ start=Position(
+ line=self.start.line + lines, character=self.start.character
+ ),
+ end=Position(line=self.end.line + lines, character=self.end.character),
+ )
+
+ def contains(self, position: Position) -> bool:
+ return self.start <= position and position <= self.end
+
+ def merge_with(self, other: "Range") -> "Range":
+ return Range(
+ start=min(self.start, other.start).copy(),
+ end=max(self.end, other.end).copy(),
+ )
+
+ @staticmethod
+ def from_indices(string: str, start_index: int, end_index: int) -> "Range":
+ return Range(
+ start=Position.from_index(string, start_index),
+ end=Position.from_index(string, end_index),
+ )
+
+ @staticmethod
+ def from_shorthand(
+ start_line: int, start_char: int, end_line: int, end_char: int
+ ) -> "Range":
+ return Range(
+ start=Position(line=start_line, character=start_char),
+ end=Position(line=end_line, character=end_char),
+ )
+
+ @staticmethod
+ def from_entire_file(content: str) -> "Range":
+ lines = content.splitlines()
+ if len(lines) == 0:
+ return Range.from_shorthand(0, 0, 0, 0)
+ return Range.from_shorthand(0, 0, len(lines), 0)
+
+ @staticmethod
+ def from_snippet_in_file(content: str, snippet: str) -> "Range":
+ start_index = content.index(snippet)
+ end_index = start_index + len(snippet)
+ return Range.from_indices(content, start_index, end_index)
+
+ @staticmethod
+ def from_lines_snippet_in_file(content: str, snippet: str) -> "Range":
+ # lines is a substring of the content modulo whitespace on each line
+ content_lines = content.splitlines()
+ snippet_lines = snippet.splitlines()
+
+ start_line = -1
+ end_line = -1
+ looking_for_line = 0
+ for i in range(len(content_lines)):
+ if content_lines[i].strip() == snippet_lines[looking_for_line].strip():
+ if looking_for_line == len(snippet_lines) - 1:
+ start_line = i - len(snippet_lines) + 1
+ end_line = i
+ break
+ looking_for_line += 1
+ else:
+ looking_for_line = 0
+
+ if start_line == -1 or end_line == -1:
+ raise ValueError("Snippet not found in content")
+
+ return Range.from_shorthand(
+ start_line, 0, end_line, len(content_lines[end_line]) - 1
+ )
+
+ @staticmethod
+ def from_position(position: Position) -> "Range":
+ return Range(start=position, end=position)
+
+
+class AbstractModel(ABC, BaseModel):
+ @root_validator(pre=True)
+ def check_is_subclass(cls, values):
+ if not issubclass(cls, AbstractModel):
+ raise TypeError(
+ "AbstractModel subclasses must be subclasses of AbstractModel"
+ )
+
+
+class TracebackFrame(BaseModel):
+ filepath: str
+ lineno: int
+ function: str
+ code: Union[str, None]
+
+ def __eq__(self, other):
+ return (
+ self.filepath == other.filepath
+ and self.lineno == other.lineno
+ and self.function == other.function
+ )
+
+
+class Traceback(BaseModel):
+ frames: List[TracebackFrame]
+ message: str
+ error_type: str
+ full_traceback: Union[str, None]
+
+ @classmethod
+ def from_tbutil_parsed_exc(cls, tbutil_parsed_exc):
+ return cls(
+ frames=[
+ TracebackFrame(
+ filepath=frame["filepath"],
+ lineno=frame["lineno"],
+ function=frame["funcname"],
+ code=frame["source_line"],
+ )
+ for frame in tbutil_parsed_exc.frames
+ ],
+ message=tbutil_parsed_exc.exc_msg,
+ error_type=tbutil_parsed_exc.exc_type,
+ full_traceback=tbutil_parsed_exc.to_string(),
+ )
diff --git a/server/continuedev/models/reference/generate.py b/server/continuedev/models/reference/generate.py
new file mode 100644
index 00000000..74912f75
--- /dev/null
+++ b/server/continuedev/models/reference/generate.py
@@ -0,0 +1,144 @@
+import html
+import importlib
+import json
+from textwrap import dedent
+
+LLM_MODULES = [
+ ("openai", "OpenAI"),
+ ("anthropic", "AnthropicLLM"),
+ ("ggml", "GGML"),
+ ("llamacpp", "LlamaCpp"),
+ ("text_gen_interface", "TextGenUI"),
+ ("ollama", "Ollama"),
+ ("replicate", "ReplicateLLM"),
+ ("together", "TogetherLLM"),
+ ("hf_inference_api", "HuggingFaceInferenceAPI"),
+ ("hf_tgi", "HuggingFaceTGI"),
+ ("openai_free_trial", "OpenAIFreeTrial"),
+ ("google_palm_api", "GooglePaLMAPI"),
+ ("queued", "QueuedLLM"),
+]
+
+CONTEXT_PROVIDER_MODULES = [
+ ("diff", "DiffContextProvider"),
+ ("file", "FileContextProvider"),
+ ("filetree", "FileTreeContextProvider"),
+ ("github", "GitHubIssuesContextProvider"),
+ ("google", "GoogleContextProvider"),
+ ("search", "SearchContextProvider"),
+ ("terminal", "TerminalContextProvider"),
+ ("url", "URLContextProvider"),
+]
+
+
+def import_llm_module(module_name, module_title):
+ module_name = f"continuedev.libs.llm.{module_name}"
+ module = importlib.import_module(module_name)
+ obj = getattr(module, module_title)
+ return obj
+
+
+def import_context_provider_module(module_name, module_title):
+ module_name = f"continuedev.plugins.context_providers.{module_name}"
+ module = importlib.import_module(module_name)
+ obj = getattr(module, module_title)
+ return obj
+
+
+def docs_from_schema(schema, filepath, ignore_properties=[], inherited=[]):
+ # Generate markdown docs
+ properties = ""
+ inherited_properties = ""
+
+ def add_property(prop, details, only_required):
+ required = prop in schema.get("required", [])
+ if only_required != required or prop in ignore_properties:
+ return ""
+ required = "true" if required else "false"
+ return f"""<ClassPropertyRef name='{prop}' details='{html.escape(json.dumps(details))}' required={{{required}}} default="{html.escape(str(details.get("default", "")))}"/>\n"""
+
+ for prop, details in schema["properties"].items():
+ property = add_property(prop, details, True)
+ if prop in inherited:
+ inherited_properties += property
+ else:
+ properties += property
+
+ for prop, details in schema["properties"].items():
+ property = add_property(prop, details, False)
+ if prop in inherited:
+ inherited_properties += property
+ else:
+ properties += property
+
+ return dedent(
+ f"""\
+import ClassPropertyRef from '@site/src/components/ClassPropertyRef.tsx';
+
+# {schema['title']}
+
+{dedent(schema.get("description", ""))}
+
+[View the source](https://github.com/continuedev/continue/tree/main/continuedev/src/continuedev/{filepath})
+
+## Properties
+
+{properties}
+
+### Inherited Properties
+
+{inherited_properties}"""
+ )
+
+
+llm_module = importlib.import_module("continuedev.libs.llm.base")
+ctx_obj = getattr(llm_module, "LLM")
+schema = ctx_obj.schema()
+ctx_properties = schema["properties"].keys()
+
+for module_name, module_title in LLM_MODULES:
+ obj = import_llm_module(module_name, module_title)
+ schema = obj.schema()
+ markdown_docs = docs_from_schema(
+ schema, f"libs/llm/{module_name}.py", inherited=ctx_properties
+ )
+ with open(f"docs/docs/reference/Models/{module_title.lower()}.md", "w") as f:
+ f.write(markdown_docs)
+
+config_module = importlib.import_module("continuedev.core.config")
+config_obj = getattr(config_module, "ContinueConfig")
+schema = config_obj.schema()
+markdown_docs = docs_from_schema(schema, "core/config.py")
+with open("docs/docs/reference/config.md", "w") as f:
+ f.write(markdown_docs)
+
+ctx_module = importlib.import_module("continuedev.core.context")
+ctx_obj = getattr(ctx_module, "ContextProvider")
+schema = ctx_obj.schema()
+ctx_properties = schema["properties"].keys()
+for module_name, module_title in CONTEXT_PROVIDER_MODULES:
+ obj = import_context_provider_module(module_name, module_title)
+ schema = obj.schema()
+ markdown_docs = docs_from_schema(
+ schema,
+ f"plugins/context_providers/{module_name}.py",
+ ignore_properties=[
+ "sdk",
+ "updated_documents",
+ "delete_documents",
+ "selected_items",
+ "ignore_patterns",
+ ],
+ inherited=ctx_properties,
+ )
+ with open(
+ f"docs/docs/reference/Context Providers/{module_title.lower()}.md", "w"
+ ) as f:
+ f.write(markdown_docs)
+
+# sdk_module = importlib.import_module("continuedev.core.sdk")
+# sdk_obj = getattr(sdk_module, "ContinueSDK")
+# schema = sdk_obj.schema()
+# markdown_docs = docs_from_schema(schema, "sdk", ignore_properties=[])
+# with open("docs/docs/reference/ContinueSDK.md", "w") as f:
+# f.write(markdown_docs)