aboutsummaryrefslogtreecommitdiff
path: root/core
diff options
context:
space:
mode:
authormitchell <70453897+667e-11@users.noreply.github.com>2008-12-21 12:50:35 -0500
committermitchell <70453897+667e-11@users.noreply.github.com>2008-12-21 12:50:35 -0500
commite01136849a2dd6cf2c40b50d0d7ca565c24e8ed9 (patch)
tree44f4e5fc900406a92ae41314dd74a5eb1514556e /core
parentaf1cf8a2b1a63dc2d87eb311fafac44de2ee03a1 (diff)
downloadtextadept-e01136849a2dd6cf2c40b50d0d7ca565c24e8ed9.tar.gz
textadept-e01136849a2dd6cf2c40b50d0d7ca565c24e8ed9.zip
Added an actual project manager for the PM browser.
Diffstat (limited to 'core')
-rw-r--r--core/ext/pm.lua2
-rw-r--r--core/ext/pm/project_browser.lua468
2 files changed, 469 insertions, 1 deletions
diff --git a/core/ext/pm.lua b/core/ext/pm.lua
index b25ca5d8..c1638738 100644
--- a/core/ext/pm.lua
+++ b/core/ext/pm.lua
@@ -51,7 +51,7 @@
-- @usage Add additional browsers to this list.
local browsers = {
'buffer_browser', 'file_browser', 'ctags_browser', 'macro_browser',
- 'find_browser', 'modules_browser'
+ 'find_browser', 'modules_browser', 'project_browser'
}
for _, b in ipairs(browsers) do require('ext/pm.'..b) end
diff --git a/core/ext/pm/project_browser.lua b/core/ext/pm/project_browser.lua
new file mode 100644
index 00000000..4e96dab9
--- /dev/null
+++ b/core/ext/pm/project_browser.lua
@@ -0,0 +1,468 @@
+-- Copyright 2007-2008 Mitchell mitchell<att>caladbolg.net. See LICENSE.
+
+---
+-- Browser template for the Textadept project manager.
+-- It is enabled with the prefix 'project' in the project manager entry field.
+module('textadept.pm.browsers.project', package.seeall)
+
+local lfs = require 'lfs'
+local os = require 'os'
+
+local project_file -- path to the project file
+
+---
+-- Table containing the directory structure of the project.
+-- @class table
+-- @name project_root
+local project_root
+
+-- functions
+local load_project, update_project, get_parent_folder, get_live_parent_folder
+local refresh_view = textadept.pm.activate
+
+--- Matches 'project'.
+function matches(entry_text)
+ return entry_text:sub(1, 7) == 'project'
+end
+
+---
+-- Returns the files in the parent directory or project root.
+-- If the directory is a live folder, get the directory contents from the
+-- filesystem and add them to the 'project' table. Any directories are labeled
+-- as live folders for the same process.
+-- Displays nice names only (all characters after last '/' or '\')
+function get_contents_for(full_path)
+ local contents = {}
+ if project_file then
+ local project_folder = get_parent_folder(full_path)
+ if not project_folder.is_live_folder then
+ for k, v in pairs(project_folder) do
+ if type(k) == 'number' then -- file
+ contents[v] = { text = v:match('[^/\\]+$') }
+ else -- folder
+ contents[k] = {
+ parent = true,
+ text = k:match('[^/\\]+$'),
+ pixbuf = 'gtk-directory'
+ }
+ end
+ end
+ else
+ local dirpath = full_path[#full_path]
+ for name in lfs.dir(dirpath) do
+ if not name:match('^%.') then -- ignore hidden files
+ local filepath = dirpath..'/'..name
+ contents[filepath] = { text = name }
+ if lfs.attributes(dirpath..'/'..name, 'mode') == 'directory' then
+ contents[filepath].parent = true
+ contents[filepath].pixbuf = 'gtk-directory'
+ project_folder[filepath] = { is_live_folder = true }
+ else
+ project_folder[#project_folder + 1] = name
+ end
+ end
+ end
+ end
+ end
+ return contents
+end
+
+--- Opens the selected project file.
+function perform_action(selected_item)
+ textadept.io.open( selected_item[#selected_item] )
+ view:focus()
+end
+
+--- Displays the project manager context menu.
+function get_context_menu(selected_item)
+ return {
+ 'separator', -- make it harder to click 'New Project' by mistake
+ '_New Project',
+ '_Open Project',
+ '_Close Project',
+ 'separator',
+ 'Add New File',
+ 'Add Existing Files',
+ 'Add New Directory',
+ 'Add Existing Director_y',
+ '_Delete',
+ '_Rename',
+ }
+end
+
+function perform_menu_action(menu_item, selected_item)
+ if menu_item == 'New Project' then
+ -- Close all open files and prompt the user to save a project file.
+ if textadept.io.close_all() then
+ local file = cocoa_dialog( 'filesave', {
+ title = 'Save Project',
+ ['with-directory'] = (buffer.filename or ''):match('.+[/\\]'),
+ ['no-newline'] = true
+ } )
+ if #file > 0 then
+ project_file = file
+ project_root = {}
+ update_project()
+ end
+ end
+
+ elseif menu_item == 'Open Project' then
+ -- Close all open files and prompt the user for a project file to open.
+ if textadept.io.close_all() then
+ local file = cocoa_dialog( 'fileselect', {
+ title = 'Open Project',
+ ['with-directory'] = (buffer.filename or ''):match('.+[/\\]'),
+ ['no-newline'] = true
+ } )
+ if #file > 0 then
+ load_project(file)
+ refresh_view()
+ end
+ end
+
+ elseif menu_item == 'Close Project' then
+ -- Close all open files and clear the project variables.
+ if textadept.io.close_all() then
+ project_file = nil
+ project_root = nil
+ textadept.pm.clear()
+ end
+
+ elseif menu_item == 'Add New File' then
+ -- If a project is open, prompts the user to save the new file.
+ if project_file then
+ local dir = get_live_parent_folder(selected_item)
+ local file = cocoa_dialog( 'filesave', {
+ title = 'Save File',
+ ['with-directory'] = dir or project_file:match('.+[/\\]'),
+ ['no-newline'] = true
+ } )
+ if #file > 0 then
+ local function add_file_to(pfolder)
+ local exists = false
+ for _, v in ipairs(pfolder) do
+ if v == file then exists = true break end
+ end
+ if not exists then pfolder[#pfolder + 1] = file end
+ update_project()
+ refresh_view()
+ end
+ local project_folder = get_parent_folder(selected_item)
+ if not project_folder.is_live_folder then
+ add_file_to(project_folder)
+ else
+ -- If the user is saving to a different folder than was selected,
+ -- caution them about unexpected behavior and ask to save in the
+ -- project root instead.
+ if dir and file:match('^(.+)/') ~= dir then
+ local ret = cocoa_dialog( 'yesno-msgbox', {
+ text = 'Add to Project Root Instead?',
+ ['informative-text'] = 'You are adding a new file to a live '..
+ 'folder which may not show up if the filepaths do not match.'..
+ '\nAdd the file to the project root instead?',
+ ['no-newline'] = true
+ } )
+ if ret == '1' then add_file_to(project_root) end
+ if ret == '3' then return end
+ end
+ end
+ local f = io.open(file, 'w') f:write('') f:close()
+ update_project()
+ refresh_view()
+ end
+ end
+
+ elseif menu_item == 'Add Existing Files' then
+ -- If a project is open, prompts the user to add existing files.
+ -- If the directory the files are being added to is a live folder, the user
+ -- is asked to add the files to the project root instead of the live folder
+ -- because adding to the latter is not possible.
+ -- Files are added if they do not already exist in the project. This does
+ -- not always apply when live folders are in a project.
+ if project_file then
+ local files = cocoa_dialog( 'fileselect', {
+ title = 'Select Files',
+ text = 'Select files to add to the project',
+ -- in Windows, dialog:get_filenames() is unavailable; only allow single
+ -- selection
+ ['select-multiple'] = not WIN32 or nil,
+ ['with-directory'] = (buffer.filename or project_file):match('.+[/\\]')
+ } )
+ if #files > 0 then
+ local function add_files_to(pfolder)
+ for file in files:gmatch('[^\n]+') do
+ local exists = false
+ for _, v in ipairs(pfolder) do
+ if v == file then exists = true break end
+ end
+ if not exists then pfolder[#pfolder + 1] = file end
+ end
+ update_project()
+ refresh_view()
+ end
+ local project_folder = get_parent_folder(selected_item)
+ if not project_folder.is_live_folder then
+ add_files_to(project_folder)
+ else
+ if cocoa_dialog( 'yesno-msgbox', {
+ text = 'Add to Project Root Instead?',
+ ['informative-text'] = 'You are adding existing files to a live '..
+ 'folder which is not possible.\nAdd them to the project root '..
+ 'instead?',
+ ['no-newline'] = true,
+ } ) == '1' then add_files_to(project_root) end
+ end
+ end
+ end
+
+ elseif menu_item == 'Add New Directory' then
+ -- If a project is open, prompts the user for a directory name to add.
+ -- This only works if the directory the directory is being added to is not a
+ -- live directory.
+ -- The directory is added if it does not already exist in the project. This
+ -- does not always apply when live folders are in a project.
+ if project_file then
+ local ret, name = cocoa_dialog( 'standard-inputbox', {
+ ['informative-text'] = 'Directory Name?',
+ ['no-newline'] = true
+ } ):match('^(%d)\n([^\n]+)$')
+ if ret == '1' and name and #name > 0 then
+ local project_folder = get_parent_folder(selected_item)
+ if not project_folder.is_live_folder then
+ if not project_folder[name] then project_folder[name] = {} end
+ else
+ lfs.mkdir( get_live_parent_folder(selected_item)..'/'..name )
+ end
+ update_project()
+ refresh_view()
+ end
+ end
+
+ elseif menu_item == 'Add Existing Directory' then
+ -- If a project is open, prompts the user for an existing directory to add.
+ -- If the directory the directory being added to is a live folder, the user
+ -- is asked to add the directory to the project root instead of the live
+ -- folder because adding to the latter is not possible.
+ -- The directory is added if it does not already exist in the project. This
+ -- does not always apply when live folders are in a project.
+ if project_file then
+ local dir = cocoa_dialog( 'fileselect', {
+ title = 'Select Directory',
+ text = 'Select a directory to add to the project',
+ ['select-only-directories'] = true,
+ ['with-directory'] = (buffer.filename or ''):match('.+[/\\]'),
+ ['no-newline'] = true
+ } )
+ if #dir > 0 then
+ local function add_directory_to(pfolder)
+ if not pfolder[dir] then
+ pfolder[dir] = { is_live_folder = true }
+ update_project()
+ refresh_view()
+ end
+ end
+ local project_folder = get_parent_folder(selected_item)
+ if not project_folder.is_live_folder then
+ add_directory_to(project_folder)
+ else
+ if cocoa_dialog( 'yesno-msgbox', {
+ text = 'Add to Project Root Instead?',
+ ['informative-text'] = 'You are adding an existing directory to '..
+ 'a live folder which is not possible.\nAdd it to the project '..
+ 'root instead?',
+ ['no-newline'] = true,
+ } ) == '1' then add_directory_to(project_root) end
+ end
+ end
+ end
+
+ elseif menu_item == 'Delete' then
+ -- If a project is open, deletes the file from the project unless it is
+ -- contained in a live folder.
+ if project_file then
+ local project_folder = get_parent_folder(selected_item)
+ local item = selected_item[#selected_item]
+ if not project_folder.is_live_folder then
+ if project_folder[item] then -- directory
+ table.remove(selected_item, #selected_item)
+ local parent_folder = get_parent_folder(selected_item)
+ parent_folder[item] = nil
+ else -- file
+ for i, file in ipairs(project_folder) do
+ if file == item then
+ local ret = cocoa_dialog( 'yesno-msgbox', {
+ text = 'Keep on Disk?',
+ ['informative-text'] = 'This file will be removed from the '..
+ 'project.\nLeave it on your computer? If not, it will be '..
+ 'permanently deleted.',
+ ['no-newline'] = true
+ } )
+ if ret == '2' then os.remove(file) end
+ if ret == '3' then return end
+ table.remove(project_folder, i)
+ break
+ end
+ end
+ end
+ else
+ local function remove_directory(dirpath)
+ for name in lfs.dir(dirpath) do
+ if not name:match('^%.%.?$') then os.remove(dirpath..'/'..name) end
+ end
+ lfs.rmdir(dirpath)
+ end
+ local item_is_dir = lfs.attributes(item, 'mode') == 'directory'
+ -- If the selection to delete is a live folder and the parent is not,
+ -- ask the user if they want to delete it permanently in addition to
+ -- removing it from the project.
+ table.remove(selected_item, #selected_item)
+ local parent_folder = get_parent_folder(selected_item)
+ if item_is_dir and not parent_folder.is_live_folder then
+ local ret = cocoa_dialog( 'yesno-msgbox', {
+ text = 'Keep on Disk?',
+ ['informative-text'] = 'This directory will be removed from the '..
+ 'project.\nLeave it on your computer? If not, it will be '..
+ 'permanently deleted.',
+ ['no-newline'] = true
+ } )
+ if ret == '2' then remove_directory(item) end
+ if ret == '3' then return end
+ parent_folder[item] = nil
+ else
+ if cocoa_dialog( 'msgbox', {
+ text = 'Delete Permanently?',
+ ['informative-text'] = 'You have selected a file from a live '..
+ 'folder to delete.\nIt will be deleted permanently. Continue?\n'..
+ '(To delete a live folder from the project, select the highest '..
+ 'level live folder.)',
+ ['no-newline'] = true,
+ button1 = 'No',
+ button2 = 'Yes',
+ button3 = 'Cancel',
+ } ) ~= '2' then return end
+ if item_is_dir then remove_directory(item) else os.remove(item) end
+ end
+ end
+ update_project()
+ refresh_view()
+ end
+
+ elseif menu_item == 'Rename' then
+ -- If a project is open, prompts the user for a new file/directory name.
+ if project_file then
+ local ret, name = cocoa_dialog( 'standard-inputbox', {
+ ['informative-text'] = 'New Name?',
+ ['no-newline'] = true
+ } ):match('^(%d)\n([^\n]+)$')
+ if ret == '1' and name and #name > 0 then
+ local oldname = selected_item[#selected_item]
+ local newname = oldname:match('^.+[/\\]')..name
+ local project_folder = get_parent_folder(selected_item)
+ if not project_folder.is_live_folder then
+ if lfs.attributes(oldname, 'mode') == 'directory' then
+ table.remove(selected_item, #selected_item)
+ local parent_folder = get_parent_folder(selected_item)
+ parent_folder[newname] = parent_folder[oldname]
+ parent_folder[oldname] = nil
+ else
+ for i, file in ipairs(project_folder) do
+ if file == oldname then table.remove(project_folder, i) break end
+ end
+ project_folder[#project_folder + 1] = newname
+ end
+ else
+ -- If the directory being renamed is live directory and the parent is
+ -- not, rename it through the parent.
+ -- (If the live directory is not top level or the file is in a live
+ -- directory, refresh_view() will be enough.)
+ if lfs.attributes(oldname, 'mode') == 'directory' then
+ table.remove(selected_item, #selected_item)
+ local parent_folder = get_parent_folder(selected_item)
+ if not parent_folder.is_live_folder then
+ parent_folder[newname] = { is_live_folder = true }
+ parent_folder[oldname] = nil
+ end
+ end
+ end
+ os.rename(oldname, newname)
+ update_project()
+ refresh_view()
+ end
+ end
+
+ end
+end
+
+---
+-- [Local function] Loads a given project file.
+-- Sets the local 'project_file' and 'project' fields appropriately.
+-- @param file The project file.
+load_project = function(file)
+ local f = io.open(file)
+ project_root = loadstring('return '..f:read('*all'))()
+ f:close()
+ project_file = file
+end
+
+---
+-- [Local function] Writes the current project to the project file.
+update_project = function()
+ local function write_folder(folder, f)
+ for k, v in pairs(folder) do
+ if type(k) == 'number' then -- file
+ f:write("'"..v.."',\n")
+ else -- directory
+ f:write("['"..k.."'] = {\n")
+ if not v.is_live_folder then
+ write_folder(v, f)
+ else
+ f:write("is_live_folder = true")
+ end
+ f:write("\n},\n")
+ end
+ end
+ end
+ local f = io.open(project_file, 'w')
+ f:write('{\n')
+ write_folder(project_root, f)
+ f:write('}')
+ f:close()
+end
+
+---
+-- [Local function] If the selected item is a folder, returns that; otherwise
+-- returns the parent folder of the selected item.
+-- Removes toplevel project manager entry text if necessary.
+-- @param full_path The full_path or selected_item as given by:
+-- get_contents_for, perform_action, get_context_menu, perform_menu_action.
+-- @return the table object from the local 'project' field.
+get_parent_folder = function(full_path)
+ if full_path[1] and full_path[1]:sub(1, 7) == 'project' then
+ table.remove(full_path, 1)
+ end
+ local pfolder = project_root
+ for _, folder in ipairs(full_path) do
+ if pfolder[folder] then pfolder = pfolder[folder] end
+ end
+ return pfolder
+end
+
+---
+-- [Local function] If the selected item is a live folder, returns its path;
+-- otherwise returns the path of the live parent folder of the selected
+-- item.
+-- @param full_path The full_path or selected_item as given by:
+-- get_contents_for, perform_action, get_context_menu, perform_menu_action.
+-- @return string path or nil.
+get_live_parent_folder = function(selected_item)
+ local dir = nil
+ if get_parent_folder(selected_item).is_live_folder then
+ if lfs.attributes(
+ selected_item[#selected_item], 'mode' ) == 'directory' then
+ dir = selected_item[#selected_item]
+ else
+ dir = selected_item[#selected_item - 1]
+ end
+ end
+ return dir
+end